L'entreprise Olist souhaite que nous fournissions à ses équipes
d'e-commerce une segmentation des clients qu’elles pourront utiliser
au quotidien pour leurs campagnes de communication.
Il y en a 3 :
# pip install flake8 pycodestyle_magic
# %load_ext pycodestyle_magic # Verifies that PEP8 is respected
# %pycodestyle_on
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from math import pi
import pickle
import re
from sklearn.preprocessing import PowerTransformer, MinMaxScaler,\
StandardScaler, RobustScaler
from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA
from scipy.cluster.hierarchy import dendrogram
from yellowbrick.cluster import KElbowVisualizer, SilhouetteVisualizer
!pip install --upgrade category_encoders
import category_encoders as ce
Requirement already up-to-date: category_encoders in c:\users\waldu\anaconda3\lib\site-packages (2.2.2) Requirement already satisfied, skipping upgrade: patsy>=0.5.1 in c:\users\waldu\anaconda3\lib\site-packages (from category_encoders) (0.5.1) Requirement already satisfied, skipping upgrade: scikit-learn>=0.20.0 in c:\users\waldu\anaconda3\lib\site-packages (from category_encoders) (0.23.2) Requirement already satisfied, skipping upgrade: statsmodels>=0.9.0 in c:\users\waldu\anaconda3\lib\site-packages (from category_encoders) (0.12.0) Requirement already satisfied, skipping upgrade: numpy>=1.14.0 in c:\users\waldu\anaconda3\lib\site-packages (from category_encoders) (1.19.2) Requirement already satisfied, skipping upgrade: pandas>=0.21.1 in c:\users\waldu\anaconda3\lib\site-packages (from category_encoders) (1.1.3) Requirement already satisfied, skipping upgrade: scipy>=1.0.0 in c:\users\waldu\anaconda3\lib\site-packages (from category_encoders) (1.5.2) Requirement already satisfied, skipping upgrade: six in c:\users\waldu\anaconda3\lib\site-packages (from patsy>=0.5.1->category_encoders) (1.15.0) Requirement already satisfied, skipping upgrade: joblib>=0.11 in c:\users\waldu\anaconda3\lib\site-packages (from scikit-learn>=0.20.0->category_encoders) (0.17.0) Requirement already satisfied, skipping upgrade: threadpoolctl>=2.0.0 in c:\users\waldu\anaconda3\lib\site-packages (from scikit-learn>=0.20.0->category_encoders) (2.1.0) Requirement already satisfied, skipping upgrade: pytz>=2017.2 in c:\users\waldu\anaconda3\lib\site-packages (from pandas>=0.21.1->category_encoders) (2020.1) Requirement already satisfied, skipping upgrade: python-dateutil>=2.7.3 in c:\users\waldu\anaconda3\lib\site-packages (from pandas>=0.21.1->category_encoders) (2.8.1)
# Changing Pandas settings for
# be able to display more rows and more columns.
pd.set_option("max_rows", 200)
pd.set_option("display.max_columns", 200)
pd.set_option('display.float_format', lambda x: '%.5f' % x)
# Deactivating Warnings
pd.options.mode.chained_assignment = None # default='warn'
def plot_dendrogram(model, **kwargs):
'''Create linkage matrix and then plot the dendrogram'''
# create the counts of samples under each node
counts = np.zeros(model.children_.shape[0])
n_samples = len(model.labels_)
for i, merge in enumerate(model.children_):
current_count = 0
for child_idx in merge:
if child_idx < n_samples:
current_count += 1 # leaf node
else:
current_count += counts[child_idx - n_samples]
counts[i] = current_count
linkage_matrix = np.column_stack([model.children_, model.distances_,
counts]).astype(float)
# Plot the corresponding dendrogram
dendrogram(linkage_matrix, **kwargs)
def exploreFrequencies(myDF):
'''
Takes as input the DataFrame myDF
Displays the output for each column of myDF:
- The number of unique values
- The number of missing values
This function requires that the DataFrame
has its characteristic 'name' entered beforehand.'''
print("{0:30} {1:25} {2:25}".format(myDF.name,
"unique values",
"missing values"))
for i in myDF:
print("{0:30} {1:20} {2:20}".format(i,
myDF[i].nunique(),
myDF[i].isna().sum()))
print("------------------------------------")
def calcAverageBasket(data):
'''
Calculate and return the average basket value per customer
The average basket corresponds to the total amount ordered by
the customer divided by the total number of orders.
Formula: BasketAverage = TotalOrder / NbOrder
The total amount ordered by the customer is present
in the variable <totalOrd>.
If the information is not present (is 0) then
I use the data present in <totalPaid>.
which includes shipping costs.
The total number of orders (including the value of the product
is filled in) is present in the variable <nbPrices>.
If no product value is entered then
I use the variable <nbOrders>.
First define the numerator and then
the denominator of the formula before calculating
and return the value of the average basket.
'''
totalOrd = data['total_amout_ordered']
totalPaid = data['total_amout_paid']
nbPrices = data['number_of_prices_filled_in']
nbOrders = data['total_of_orders']
if totalOrd > 0:
numerator = totalOrd
else:
numerator = totalPaid
if nbPrices > 0:
denominator = nbPrices
else:
denominator = nbOrders
return numerator/denominator
Olist nous fournit une base de données anonymisée
comportant des informations sur l’historique de commandes,
les produits achetés, les commentaires de satisfaction,
et la localisation des clients depuis janvier 2017.
Voici le schéma de la base de données d'Olist :

J'importe chaque base de donnée dans un DataFrame distinct :
df_customer = pd.read_csv('data/olist_customers_dataset.csv',
sep=',')
df_orders = pd.read_csv('data/olist_orders_dataset.csv',
sep=',')
df_reviews = pd.read_csv('data/olist_order_reviews_dataset.csv',
sep=',')
df_payments = pd.read_csv('data/olist_order_payments_dataset.csv',
sep=',')
df_items = pd.read_csv('data/olist_order_items_dataset.csv',
sep=',')
df_products = pd.read_csv('data/olist_products_dataset.csv',
sep=',')
df_translation = pd.read_csv('data/product_category_name_translation.csv',
sep=',')
df_sellers = pd.read_csv('data/olist_sellers_dataset.csv',
sep=',')
Je merge l'ensemble des DataFrame,
en réalisant systématiquement une **jointure à gauche**,
en partant du DataFrame df_customer :
df = pd.merge(df_customer, df_orders,
how='left',
on='customer_id')\
.merge(df_payments,
how='left',
on='order_id')\
.merge(df_items,
how='left',
on='order_id')\
.merge(df_products,
how='left',
on='product_id')\
.merge(df_translation,
how='left',
on='product_category_name')\
.merge(df_sellers,
how='left',
on='seller_id')
df.head(4).T
| 0 | 1 | 2 | 3 | |
|---|---|---|---|---|
| customer_id | 06b8999e2fba1a1fbc88172c00ba8bc7 | 18955e83d337fd6b2def6b18a428ac77 | 4e7b3e00288586ebd08712fdd0374a03 | b2b6027bc5c5109e529d4dc6358b12c3 |
| customer_unique_id | 861eff4711a542e4b93843c6dd7febb0 | 290c77bc529b7ac935b93aa66c333dc3 | 060e732b5b29e8181a18229c7b0b2b5e | 259dac757896d24d7702b9acbbff3f3c |
| customer_zip_code_prefix | 14409 | 9790 | 1151 | 8775 |
| customer_city | franca | sao bernardo do campo | sao paulo | mogi das cruzes |
| customer_state | SP | SP | SP | SP |
| order_id | 00e7ee1b050b8499577073aeb2a297a1 | 29150127e6685892b6eab3eec79f59c7 | b2059ed67ce144a36e2aa97d2c9e9ad2 | 951670f92359f4fe4a63112aa7306eba |
| order_status | delivered | delivered | delivered | delivered |
| order_purchase_timestamp | 2017-05-16 15:05:35 | 2018-01-12 20:48:24 | 2018-05-19 16:07:45 | 2018-03-13 16:06:38 |
| order_approved_at | 2017-05-16 15:22:12 | 2018-01-12 20:58:32 | 2018-05-20 16:19:10 | 2018-03-13 17:29:19 |
| order_delivered_carrier_date | 2017-05-23 10:47:57 | 2018-01-15 17:14:59 | 2018-06-11 14:31:00 | 2018-03-27 23:22:42 |
| order_delivered_customer_date | 2017-05-25 10:35:35 | 2018-01-29 12:41:19 | 2018-06-14 17:58:51 | 2018-03-28 16:04:25 |
| order_estimated_delivery_date | 2017-06-05 00:00:00 | 2018-02-06 00:00:00 | 2018-06-13 00:00:00 | 2018-04-10 00:00:00 |
| payment_sequential | 1.00000 | 1.00000 | 1.00000 | 1.00000 |
| payment_type | credit_card | credit_card | credit_card | credit_card |
| payment_installments | 2.00000 | 8.00000 | 7.00000 | 1.00000 |
| payment_value | 146.87000 | 335.48000 | 157.73000 | 173.30000 |
| order_item_id | 1.00000 | 1.00000 | 1.00000 | 1.00000 |
| product_id | a9516a079e37a9c9c36b9b78b10169e8 | 4aa6014eceb682077f9dc4bffebc05b0 | bd07b66896d6f1494f5b86251848ced7 | a5647c44af977b148e0a3a4751a09e2e |
| seller_id | 7c67e1448b00f6e969d365cea6b010ab | b8bc237ba3788b23da09c0f1f3a3288c | 7c67e1448b00f6e969d365cea6b010ab | 7c67e1448b00f6e969d365cea6b010ab |
| shipping_limit_date | 2017-05-22 15:22:12 | 2018-01-18 20:58:32 | 2018-06-05 16:19:10 | 2018-03-27 16:31:16 |
| price | 124.99000 | 289.00000 | 139.94000 | 149.94000 |
| freight_value | 21.88000 | 46.48000 | 17.79000 | 23.36000 |
| product_category_name | moveis_escritorio | utilidades_domesticas | moveis_escritorio | moveis_escritorio |
| product_name_lenght | 41.00000 | 43.00000 | 55.00000 | 48.00000 |
| product_description_lenght | 1141.00000 | 1002.00000 | 955.00000 | 1066.00000 |
| product_photos_qty | 1.00000 | 3.00000 | 1.00000 | 1.00000 |
| product_weight_g | 8683.00000 | 10150.00000 | 8267.00000 | 12160.00000 |
| product_length_cm | 54.00000 | 89.00000 | 52.00000 | 56.00000 |
| product_height_cm | 64.00000 | 15.00000 | 52.00000 | 51.00000 |
| product_width_cm | 31.00000 | 40.00000 | 17.00000 | 28.00000 |
| product_category_name_english | office_furniture | housewares | office_furniture | office_furniture |
| seller_zip_code_prefix | 8577.00000 | 88303.00000 | 8577.00000 | 8577.00000 |
| seller_city | itaquaquecetuba | itajai | itaquaquecetuba | itaquaquecetuba |
| seller_state | SP | SC | SP | SP |
Certaines traductions en anglais de catégories sont manquantes.
Je les ajoutes :
df[['product_category_name',
'product_category_name_english']] = \
df[['product_category_name',
'product_category_name_english']]\
.append(pd.DataFrame([['pc_gamer', 'pc_gamer'],
['portateis_cozinha_e_preparadores_de_alimentos',
'food_preparers_and_kitchen_portals']],
columns=['product_category_name',
'product_category_name_english']),
ignore_index=True)
Je convertis au bon format les colonnes contenant des dates :
df[['order_purchase_timestamp',
'order_approved_at',
'order_delivered_carrier_date',
'order_delivered_customer_date',
'order_estimated_delivery_date']] =\
df[['order_purchase_timestamp',
'order_approved_at',
'order_delivered_carrier_date',
'order_delivered_customer_date',
'order_estimated_delivery_date']].apply(pd.to_datetime)
df.describe().T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| customer_zip_code_prefix | 118434.00000 | 35034.26439 | 29819.29022 | 1003.00000 | 11310.00000 | 24240.00000 | 58464.50000 | 99990.00000 |
| payment_sequential | 118431.00000 | 1.09421 | 0.72829 | 1.00000 | 1.00000 | 1.00000 | 1.00000 | 29.00000 |
| payment_installments | 118431.00000 | 2.93740 | 2.77416 | 0.00000 | 1.00000 | 2.00000 | 4.00000 | 24.00000 |
| payment_value | 118431.00000 | 172.84939 | 268.25983 | 0.00000 | 60.86000 | 108.20000 | 189.24500 | 13664.08000 |
| order_item_id | 117604.00000 | 1.19592 | 0.69772 | 1.00000 | 1.00000 | 1.00000 | 1.00000 | 21.00000 |
| price | 117604.00000 | 120.82285 | 184.47737 | 0.85000 | 39.90000 | 74.90000 | 134.90000 | 6735.00000 |
| freight_value | 117604.00000 | 20.04555 | 15.86135 | 0.00000 | 13.08000 | 16.29000 | 21.19000 | 409.68000 |
| product_name_lenght | 115906.00000 | 48.76721 | 10.03516 | 5.00000 | 42.00000 | 52.00000 | 57.00000 | 76.00000 |
| product_description_lenght | 115906.00000 | 786.95730 | 653.02901 | 4.00000 | 347.00000 | 601.00000 | 985.00000 | 3992.00000 |
| product_photos_qty | 115906.00000 | 2.20785 | 1.71952 | 1.00000 | 1.00000 | 1.00000 | 3.00000 | 20.00000 |
| product_weight_g | 117584.00000 | 2114.28107 | 3788.75405 | 0.00000 | 300.00000 | 700.00000 | 1800.00000 | 40425.00000 |
| product_length_cm | 117584.00000 | 30.25687 | 16.19087 | 7.00000 | 18.00000 | 25.00000 | 38.00000 | 105.00000 |
| product_height_cm | 117584.00000 | 16.63029 | 13.45878 | 2.00000 | 8.00000 | 13.00000 | 20.00000 | 105.00000 |
| product_width_cm | 117584.00000 | 23.06839 | 11.74837 | 6.00000 | 15.00000 | 20.00000 | 30.00000 | 118.00000 |
| seller_zip_code_prefix | 117604.00000 | 24442.88648 | 27573.80299 | 1001.00000 | 6429.00000 | 13660.00000 | 28035.00000 | 99730.00000 |
J'attribue un nom à chaque DataFrame
avant d'afficher leurs caractéristiques :
df_customer.name = 'df_customer'
df_orders.name = 'df_orders'
df_payments.name = 'df_payments'
df_items.name = 'df_items'
df_products.name = 'df_products'
df_translation.name = 'df_translation'
df_sellers.name = 'df_sellers'
exploreFrequencies(df_customer)
exploreFrequencies(df_orders)
exploreFrequencies(df_payments)
exploreFrequencies(df_items)
exploreFrequencies(df_products)
exploreFrequencies(df_translation)
exploreFrequencies(df_sellers)
df_customer unique values missing values customer_id 99441 0 customer_unique_id 96096 0 customer_zip_code_prefix 14994 0 customer_city 4119 0 customer_state 27 0 ------------------------------------ df_orders unique values missing values order_id 99441 0 customer_id 99441 0 order_status 8 0 order_purchase_timestamp 98875 0 order_approved_at 90733 160 order_delivered_carrier_date 81018 1783 order_delivered_customer_date 95664 2965 order_estimated_delivery_date 459 0 ------------------------------------ df_payments unique values missing values order_id 99440 0 payment_sequential 29 0 payment_type 5 0 payment_installments 24 0 payment_value 29077 0 ------------------------------------ df_items unique values missing values order_id 98666 0 order_item_id 21 0 product_id 32951 0 seller_id 3095 0 shipping_limit_date 93318 0 price 5968 0 freight_value 6999 0 ------------------------------------ df_products unique values missing values product_id 32951 0 product_category_name 73 610 product_name_lenght 66 610 product_description_lenght 2960 610 product_photos_qty 19 610 product_weight_g 2204 2 product_length_cm 99 2 product_height_cm 102 2 product_width_cm 95 2 ------------------------------------ df_translation unique values missing values product_category_name 71 0 product_category_name_english 71 0 ------------------------------------ df_sellers unique values missing values seller_id 3095 0 seller_zip_code_prefix 2246 0 seller_city 611 0 seller_state 23 0 ------------------------------------
Listing des différents statuts
possibles pour une commande :
df_orders.order_status.value_counts()
delivered 96478 shipped 1107 canceled 625 unavailable 609 invoiced 314 processing 301 created 5 approved 2 Name: order_status, dtype: int64
Focus sur les statuts contenant
des valeurs manquantes :
orders_missing_values = (df_orders.isnull().sum())
print(orders_missing_values[orders_missing_values > 0])
order_approved_at 160 order_delivered_carrier_date 1783 order_delivered_customer_date 2965 dtype: int64
df_products.product_category_name.value_counts()
cama_mesa_banho 3029 esporte_lazer 2867 moveis_decoracao 2657 beleza_saude 2444 utilidades_domesticas 2335 automotivo 1900 informatica_acessorios 1639 brinquedos 1411 relogios_presentes 1329 telefonia 1134 bebes 919 perfumaria 868 fashion_bolsas_e_acessorios 849 papelaria 849 cool_stuff 789 ferramentas_jardim 753 pet_shop 719 eletronicos 517 construcao_ferramentas_construcao 400 eletrodomesticos 370 malas_acessorios 349 consoles_games 317 moveis_escritorio 309 instrumentos_musicais 289 eletroportateis 231 casa_construcao 225 livros_interesse_geral 216 fashion_calcados 173 moveis_sala 156 climatizacao 124 livros_tecnicos 123 telefonia_fixa 116 casa_conforto 111 market_place 104 alimentos_bebidas 104 fashion_roupa_masculina 95 moveis_cozinha_area_de_servico_jantar_e_jardim 94 sinalizacao_e_seguranca 93 construcao_ferramentas_seguranca 91 eletrodomesticos_2 90 construcao_ferramentas_jardim 88 alimentos 82 bebidas 81 construcao_ferramentas_iluminacao 78 agro_industria_e_comercio 74 industria_comercio_e_negocios 68 artigos_de_natal 65 audio 58 artes 55 fashion_underwear_e_moda_praia 53 dvds_blu_ray 48 moveis_quarto 45 construcao_ferramentas_ferramentas 39 livros_importados 31 portateis_casa_forno_e_cafe 31 pcs 30 cine_foto 28 musica 27 fashion_roupa_feminina 27 artigos_de_festas 26 fashion_esporte 19 artes_e_artesanato 19 flores 14 fraldas_higiene 12 moveis_colchao_e_estofado 10 portateis_cozinha_e_preparadores_de_alimentos 10 la_cuisine 10 tablets_impressao_imagem 9 casa_conforto_2 5 fashion_roupa_infanto_juvenil 5 pc_gamer 3 seguros_e_servicos 2 cds_dvds_musicais 1 Name: product_category_name, dtype: int64
products_missing_values = (df_products.isnull().sum())
print(products_missing_values[products_missing_values > 0])
product_category_name 610 product_name_lenght 610 product_description_lenght 610 product_photos_qty 610 product_weight_g 2 product_length_cm 2 product_height_cm 2 product_width_cm 2 dtype: int64
df_products.product_category_name.value_counts()
cama_mesa_banho 3029 esporte_lazer 2867 moveis_decoracao 2657 beleza_saude 2444 utilidades_domesticas 2335 automotivo 1900 informatica_acessorios 1639 brinquedos 1411 relogios_presentes 1329 telefonia 1134 bebes 919 perfumaria 868 fashion_bolsas_e_acessorios 849 papelaria 849 cool_stuff 789 ferramentas_jardim 753 pet_shop 719 eletronicos 517 construcao_ferramentas_construcao 400 eletrodomesticos 370 malas_acessorios 349 consoles_games 317 moveis_escritorio 309 instrumentos_musicais 289 eletroportateis 231 casa_construcao 225 livros_interesse_geral 216 fashion_calcados 173 moveis_sala 156 climatizacao 124 livros_tecnicos 123 telefonia_fixa 116 casa_conforto 111 market_place 104 alimentos_bebidas 104 fashion_roupa_masculina 95 moveis_cozinha_area_de_servico_jantar_e_jardim 94 sinalizacao_e_seguranca 93 construcao_ferramentas_seguranca 91 eletrodomesticos_2 90 construcao_ferramentas_jardim 88 alimentos 82 bebidas 81 construcao_ferramentas_iluminacao 78 agro_industria_e_comercio 74 industria_comercio_e_negocios 68 artigos_de_natal 65 audio 58 artes 55 fashion_underwear_e_moda_praia 53 dvds_blu_ray 48 moveis_quarto 45 construcao_ferramentas_ferramentas 39 livros_importados 31 portateis_casa_forno_e_cafe 31 pcs 30 cine_foto 28 musica 27 fashion_roupa_feminina 27 artigos_de_festas 26 fashion_esporte 19 artes_e_artesanato 19 flores 14 fraldas_higiene 12 moveis_colchao_e_estofado 10 portateis_cozinha_e_preparadores_de_alimentos 10 la_cuisine 10 tablets_impressao_imagem 9 casa_conforto_2 5 fashion_roupa_infanto_juvenil 5 pc_gamer 3 seguros_e_servicos 2 cds_dvds_musicais 1 Name: product_category_name, dtype: int64
productsByCategory = \
df_products.product_category_name.value_counts()
plt.figure(figsize=(15, 7))
sns.barplot(x=productsByCategory.index[:30],
y=productsByCategory.values[:30])
plt.xlabel('Categories')
plt.ylabel('Number of referenced products')
# plt.xticks(rotation=-90)
plt.xticks(rotation=45, ha='right')
plt.show()
plt.figure(figsize=(5, 10))
plt.boxplot(df_items.groupby('product_id').mean()['price'],
showfliers=False,
showmeans=True)
plt.title('Price per item')
plt.show()
df_orders_items_product = pd.merge(df_orders, df_items, on='order_id', how='left')\
.merge(df_products, on='product_id', how='left')
df_orders_items_product.product_category_name.value_counts()[:30]
cama_mesa_banho 11115 beleza_saude 9670 esporte_lazer 8641 moveis_decoracao 8334 informatica_acessorios 7827 utilidades_domesticas 6964 relogios_presentes 5991 telefonia 4545 ferramentas_jardim 4347 automotivo 4235 brinquedos 4117 cool_stuff 3796 perfumaria 3419 bebes 3065 eletronicos 2767 papelaria 2517 fashion_bolsas_e_acessorios 2031 pet_shop 1947 moveis_escritorio 1691 consoles_games 1137 malas_acessorios 1092 construcao_ferramentas_construcao 929 eletrodomesticos 771 instrumentos_musicais 680 eletroportateis 679 casa_construcao 604 livros_interesse_geral 553 alimentos 510 moveis_sala 503 casa_conforto 434 Name: product_category_name, dtype: int64
plt.figure(figsize=(15, 7))
sns.barplot(x=df_orders_items_product.product_category_name
.value_counts()[:30].index,
y=df_orders_items_product.product_category_name
.value_counts()[:30].values)
plt.xlabel('Categories')
plt.ylabel('Number of sales')
plt.xticks(rotation=45, ha='right')
plt.show()
df_orders_items_product.groupby('product_category_name')\
.sum()['price'].sort_values(ascending=False)[:30]
product_category_name beleza_saude 1258681.34000 relogios_presentes 1205005.68000 cama_mesa_banho 1036988.68000 esporte_lazer 988048.97000 informatica_acessorios 911954.32000 moveis_decoracao 729762.49000 cool_stuff 635290.85000 utilidades_domesticas 632248.66000 automotivo 592720.11000 ferramentas_jardim 485256.46000 brinquedos 483946.60000 bebes 411764.89000 perfumaria 399124.87000 telefonia 323667.53000 moveis_escritorio 273960.70000 papelaria 230943.23000 pcs 222963.13000 pet_shop 214315.41000 instrumentos_musicais 191498.88000 eletroportateis 190648.58000 eletronicos 160246.74000 consoles_games 157465.22000 fashion_bolsas_e_acessorios 152823.54000 construcao_ferramentas_construcao 144677.59000 malas_acessorios 140429.98000 eletrodomesticos_2 113317.74000 casa_construcao 83088.12000 eletrodomesticos 80171.53000 agro_industria_e_comercio 72530.47000 moveis_sala 68916.56000 Name: price, dtype: float64
amoutOfSalesByCategory = df_orders_items_product\
.groupby('product_category_name')\
.sum()['price']\
.sort_values(ascending=False)[:30]
plt.figure(figsize=(15, 7))
sns.barplot(x=amoutOfSalesByCategory.index,
y=amoutOfSalesByCategory.values)
plt.xlabel('Categories')
plt.ylabel('Amount of sales')
plt.xticks(rotation=45, ha='right')
plt.show()
plt.figure(figsize=(5, 10))
plt.boxplot(df_payments.payment_value,
showfliers=False,
showmeans=True)
plt.title('Amount of orders')
plt.show()
df_customer.customer_state.value_counts()
SP 41746 RJ 12852 MG 11635 RS 5466 PR 5045 SC 3637 BA 3380 DF 2140 ES 2033 GO 2020 PE 1652 CE 1336 PA 975 MT 907 MA 747 MS 715 PB 536 PI 495 RN 485 AL 413 SE 350 TO 280 RO 253 AM 148 AC 81 AP 68 RR 46 Name: customer_state, dtype: int64
plt.figure(figsize=(15, 7))
sns.barplot(x=df_customer.customer_state.value_counts().index,
y=df_customer.customer_state.value_counts().values)
plt.title('Number of customers / Country')
plt.xlabel('Country')
plt.ylabel('Number of customers')
plt.xticks(rotation=-45)
plt.show()
plt.figure(figsize=(15, 7))
sns.barplot(x=df_customer.customer_state
.value_counts(normalize=True).index,
y=df_customer.customer_state
.value_counts(normalize=True).values*100)
plt.title('Percentage of Customer / Country')
plt.xlabel('Country')
plt.ylabel('Percent of Customer')
plt.xticks(rotation=-45)
plt.show()
(df_reviews[['review_comment_title',
'review_comment_message']].notnull().sum()
/ len(df_reviews)) * 100
review_comment_title 11.71500 review_comment_message 41.75300 dtype: float64
df_reviews.review_score.value_counts(normalize=True)*100
5 57.42000 4 19.20000 1 11.85800 3 8.28700 2 3.23500 Name: review_score, dtype: float64
plt.figure(figsize=(8, 8))
plt.pie((df_reviews.review_score.value_counts(normalize=True)*100)
.sort_index().values,
labels=['1', '2', '3', '4', '5'],
autopct=lambda x: str(round(x, 2)) + '%',
colors=['red', 'orange', 'yellow', 'lightgreen', 'darkgreen'],
normalize=True)
plt.legend()
<matplotlib.legend.Legend at 0x23d3827e310>
Pour la suite du projet, nous nous intéressons
qu'aux commandes qui ont été livrées.
Je filtre les autres commandes (envoyées, annulées, ...)
df.order_status.value_counts()
delivered 115038 shipped 1245 canceled 745 unavailable 650 processing 375 invoiced 373 created 5 approved 3 Name: order_status, dtype: int64
df = df[df.order_status == 'delivered']
Nous allons maintenant créer/calculer de nouvelles variables
à partir des informations que nous avons à notre disposition :
L'objectif est de comprendre nos clients et d'obtenir
des informations précises, qui décrieront au mieux leurs
comportements d'achat et l'expérience qu'ils ont eu
lors de leur(s) commande(s) sur Olist.
Ainsi, pour un client donné nous allons chercher à savoir:
Indique, en jours, la différence entre la date
de livraison réelle, et la date de livraison prévisionnelle :
df['late_delivery'] = (df.order_delivered_customer_date -
df.order_estimated_delivery_date)\
.astype('timedelta64[D]')
df['late_delivery'] = df['late_delivery']*-1
Suppression des 8 commandes n'ayant pas de 'late_delivery' :
df = df[~df.late_delivery.isnull()]
On ne conserve que les moyennes des valeurs / client :
df = df.drop('late_delivery',
axis=1)\
.merge(df.groupby('customer_unique_id')
.mean()[['late_delivery']],
on='customer_unique_id',
how='left')
plt.figure(figsize=(5, 10))
plt.boxplot(df.groupby('customer_unique_id').mean()['late_delivery'],
showfliers=False,
showmeans=True)
plt.title('Delayed delivery / Customer')
plt.show()
df.payment_type.value_counts()
credit_card 84889 boleto 22362 voucher 6123 debit_card 1653 Name: payment_type, dtype: int64
Je renseigne les valeurs manquantes par l'indication '**not_defined**' :
df['payment_type'] = df['payment_type'].fillna('not_defined')
Je décide de ne conserver que l'information de paiement 'par carte'
en mutualisant les paiements par carte de crédit et carte de débit.
df.loc[df['payment_type'].str.contains('card'), 'payment_type'] = 'card'
df.payment_type.value_counts()
card 86542 boleto 22362 voucher 6123 not_defined 3 Name: payment_type, dtype: int64
Pour identifier les clients selon leur moyen de paiement préféré, je leur attribue un score :
p_type = df.payment_type.value_counts()
p_type.plot(kind='barh', title="methods of payment")
<AxesSubplot:title={'center':'methods of payment'}>
df = df.merge(df[['customer_unique_id', 'payment_type']]
.replace(p_type.index, p_type.values)
.rename(columns={'payment_type': 'payment_type_score'}),
on='customer_unique_id',
how='left')
df_sumOrd = df[['customer_unique_id', 'customer_id']]\
.groupby('customer_unique_id').nunique()
df_sumOrd.rename(columns={'customer_id': 'total_of_orders'}, inplace=True)
df_sumOrd.sort_values(by='total_of_orders', ascending=False)
| total_of_orders | |
|---|---|
| customer_unique_id | |
| 8d50f5eadf50201ccdcedfb9e2ac8455 | 15 |
| 3e43e6105506432c953e165fb2acf44c | 9 |
| 6469f99c1f9dfae7733b25662e7f1782 | 7 |
| ca77025e7201e3b30c44b472ff346268 | 7 |
| 1b6c7548a2a1f9037c1fd3ddfed95f33 | 7 |
| ... | ... |
| 565af81202a1fb8a9ffe282935381142 | 1 |
| 56583a02df6f603c331a575f4f63ec6b | 1 |
| 565811ed44e539a14bc391cf57373ed1 | 1 |
| 5657f9f721c16800a66c21081d46b18d | 1 |
| ffffd2657e2aad2907e67c3e9daecbeb | 1 |
93350 rows × 1 columns
df = df.merge(df_sumOrd, on='customer_unique_id', how='left')
plt.figure(figsize=(5, 10))
plt.boxplot(df.groupby('customer_unique_id').mean()['total_of_orders'],
showfliers=True,
showmeans=True)
plt.title('Number of orders / Customer')
plt.show()
df.total_of_orders.value_counts(normalize=False, sort=False)
1 170521 2 27607 3 3597 4 1355 5 295 6 363 7 325 9 196 15 225 Name: total_of_orders, dtype: int64
df.total_of_orders.value_counts(normalize=True, sort=False)
1 0.83391 2 0.13501 3 0.01759 4 0.00663 5 0.00144 6 0.00178 7 0.00159 9 0.00096 15 0.00110 Name: total_of_orders, dtype: float64
Plus de 83% des clients n'ont réalisés qu'une seule commande sur le site.
13.5% ont réalisé 2 commandes.
3.1% seulement des clients ont réalisés plus de 2 commandes.
df_totalAmoutOrdered = df.groupby(['customer_unique_id']).sum()\
.rename(columns={'price': 'total_amout_ordered'})
df = df.merge(df_totalAmoutOrdered['total_amout_ordered'],
on='customer_unique_id',
how='left')
plt.figure(figsize=(5, 10))
plt.boxplot(df.groupby('customer_unique_id').mean()['total_amout_ordered'],
showfliers=False,
showmeans=True)
plt.title('Total amount ordered / Customer')
plt.show()
df_totalAmoutPaid = df.groupby(['customer_unique_id']).sum()\
.rename(columns={'payment_value': 'total_amout_paid'})
df = df.merge(df_totalAmoutPaid['total_amout_paid'],
on='customer_unique_id',
how='left')
plt.figure(figsize=(5, 10))
plt.boxplot(df.groupby('customer_unique_id').mean()['total_amout_paid'],
showfliers=False,
showmeans=True)
plt.title('Total amount paid / Customer')
plt.show()
df = df.merge(df.groupby(['customer_unique_id', 'customer_id'])
.nunique().groupby('customer_unique_id').sum()['price']
.reset_index().rename(columns={'price':'number_of_prices_filled_in'}),
on='customer_unique_id', how='left')
df['average_basket'] = df.apply(lambda row: calcAverageBasket(row),
axis=1)
df.drop('number_of_prices_filled_in',
axis=1,
inplace=True)
plt.figure(figsize=(5, 10))
plt.boxplot(df.groupby('customer_unique_id').mean()['average_basket'],
showfliers=False,
showmeans=True)
plt.title('Basket Average / Customer')
plt.show()
df['day_of_order'] = df.order_approved_at.dt.weekday
d_order = df.day_of_order.value_counts(sort=False)
d_order.index = ['Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday']
d_order.plot(kind='barh', title="Order Volume / Day")
plt.show()
df['month_of_order'] = df.order_approved_at.dt.month
m_order = df.month_of_order.value_counts(sort=False)
m_order.index = ['January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December']
m_order.plot(kind='barh', title="Order Volume / Month")
plt.show()
Calcul de la date de la dernière commande existante
comme date de référence pour le calcul de la récence
de chaque commande pour chaque client :
lastOrder = df.order_approved_at.max()
df['days_since_last_order'] = (lastOrder - df.order_purchase_timestamp)\
.astype('timedelta64[D]')
On ne conserve que la valeur de la commande la plus récente :
df_recency = df[['customer_unique_id',
'days_since_last_order']]\
.groupby('customer_unique_id').min()
df = df.drop('days_since_last_order',
axis=1).merge(df_recency,
on='customer_unique_id',
how='left')
plt.figure(figsize=(5, 10))
plt.boxplot(df.groupby('customer_unique_id').mean()['days_since_last_order'],
showfliers=False,
showmeans=True)
plt.title('Number of days since last purchase')
plt.show()
df_customer_orders_reviews = df_customer\
.merge(df_orders,
on='customer_id',
how='left')\
.merge(df_reviews,
on='order_id',
how='left')\
.rename(columns={'review_score':
'customer_satisfaction'})\
.groupby('customer_unique_id').mean()
df = df.merge(df_customer_orders_reviews['customer_satisfaction'],
on='customer_unique_id',
how='left')
plt.figure(figsize=(5, 10))
sns.histplot(df.customer_satisfaction, binwidth=0.99)
plt.title('Customer Satisfaction')
plt.show()
list(df.columns)
['customer_id', 'customer_unique_id', 'customer_zip_code_prefix', 'customer_city', 'customer_state', 'order_id', 'order_status', 'order_purchase_timestamp', 'order_approved_at', 'order_delivered_carrier_date', 'order_delivered_customer_date', 'order_estimated_delivery_date', 'payment_sequential', 'payment_type', 'payment_installments', 'payment_value', 'order_item_id', 'product_id', 'seller_id', 'shipping_limit_date', 'price', 'freight_value', 'product_category_name', 'product_name_lenght', 'product_description_lenght', 'product_photos_qty', 'product_weight_g', 'product_length_cm', 'product_height_cm', 'product_width_cm', 'product_category_name_english', 'seller_zip_code_prefix', 'seller_city', 'seller_state', 'late_delivery', 'payment_type_score', 'total_of_orders', 'total_amout_ordered', 'total_amout_paid', 'average_basket', 'day_of_order', 'month_of_order', 'days_since_last_order', 'customer_satisfaction']
df.drop(['customer_id',
'customer_city',
'customer_state',
'customer_zip_code_prefix',
'order_id',
'order_status',
'order_purchase_timestamp',
'order_approved_at',
'order_delivered_carrier_date',
'order_delivered_customer_date',
'order_estimated_delivery_date',
'payment_sequential',
'payment_installments',
'payment_type',
'payment_value',
'order_item_id',
'product_id',
'seller_id',
'shipping_limit_date',
'price',
'freight_value',
'product_category_name',
'product_name_lenght',
'product_description_lenght',
'product_photos_qty',
'product_weight_g',
'product_length_cm',
'product_height_cm',
'product_width_cm',
'product_category_name_english',
'seller_zip_code_prefix',
'seller_city',
'seller_state'], axis=1, inplace=True)
df.head(4).T
| 0 | 1 | 2 | 3 | |
|---|---|---|---|---|
| customer_unique_id | 861eff4711a542e4b93843c6dd7febb0 | 290c77bc529b7ac935b93aa66c333dc3 | 060e732b5b29e8181a18229c7b0b2b5e | 259dac757896d24d7702b9acbbff3f3c |
| late_delivery | 11.00000 | 8.00000 | -1.00000 | 13.00000 |
| payment_type_score | 86542 | 86542 | 86542 | 86542 |
| total_of_orders | 1 | 1 | 1 | 1 |
| total_amout_ordered | 124.99000 | 289.00000 | 139.94000 | 149.94000 |
| total_amout_paid | 146.87000 | 335.48000 | 157.73000 | 173.30000 |
| average_basket | 124.99000 | 289.00000 | 139.94000 | 149.94000 |
| day_of_order | 1.00000 | 4.00000 | 6.00000 | 1.00000 |
| month_of_order | 5.00000 | 1.00000 | 5.00000 | 3.00000 |
| days_since_last_order | 470.00000 | 228.00000 | 101.00000 | 168.00000 |
| customer_satisfaction | 4.00000 | 5.00000 | 5.00000 | 5.00000 |
L'objectif ici est de ne conserver qu'une seule entrée / client.
Un client est identifié par son identifiant 'customer_unique_id'.
Dimension de **df** *avant* filtrage des clients :
df.shape
(204484, 11)
On ne conserve qu'un seul enregistrement de chaque client :
df.drop_duplicates('customer_unique_id', keep='last', inplace=True)
Je supprime également la colonne '**customer_unique_id**',
les clients étant maintenant identifiés par leur **Index** :
df.drop('customer_unique_id', axis=1, inplace=True)
Dimension de **df** *après* filtrage des clients :
df.shape
(93350, 10)
plt.figure(figsize=(15, 15))
sns.heatmap(df.corr(), annot=True, square=True)
plt.show()
Globalement les features ne sont pas corrélées entre-elles.
Cependant, quelques exceptions :
L'objectif à cette étape est de ramener les données sur une même échelles.
Nous essayons également d'obtenir une distribution des données qui
se rapproche le plus possible d'une distribution Normale.
df.head().T
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| late_delivery | 11.00000 | 8.00000 | -1.00000 | 13.00000 | 6.00000 |
| payment_type_score | 86542.00000 | 86542.00000 | 86542.00000 | 86542.00000 | 86542.00000 |
| total_of_orders | 1.00000 | 1.00000 | 1.00000 | 1.00000 | 1.00000 |
| total_amout_ordered | 124.99000 | 289.00000 | 139.94000 | 149.94000 | 230.00000 |
| total_amout_paid | 146.87000 | 335.48000 | 157.73000 | 173.30000 | 252.25000 |
| average_basket | 124.99000 | 289.00000 | 139.94000 | 149.94000 | 230.00000 |
| day_of_order | 1.00000 | 4.00000 | 6.00000 | 1.00000 | 6.00000 |
| month_of_order | 5.00000 | 1.00000 | 5.00000 | 3.00000 | 7.00000 |
| days_since_last_order | 470.00000 | 228.00000 | 101.00000 | 168.00000 | 31.00000 |
| customer_satisfaction | 4.00000 | 5.00000 | 5.00000 | 5.00000 | 5.00000 |
for i in df:
plt.figure(figsize=(10, 5))
sns.distplot(df[i])
plt.title(i)
C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning)
J'utilise la fonction PowerTransform
et je compare le résultat en utilisant
les méthodes Yeo-Johnson et Box-Cox.
A noter que la transformation Box-Cox
ne peut s'appliquer que sur des valeurs strictement positives.
for i in df:
plt.figure(figsize=(10, 5))
if (df[i] <= 0).sum() > 0:
plt.title(i+' - Yeo')
sns.distplot(PowerTransformer().fit_transform(df[[i]]),
label='Yeo')
else:
plt.title(i+' - Yeo - Box-Cox')
sns.distplot(PowerTransformer().fit_transform(df[[i]]), label='Yeo')
sns.distplot(PowerTransformer(method='box-cox').fit_transform(df[[i]]),
label='Box-Cox')
plt.legend()
C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) C:\Users\waldu\anaconda3\lib\site-packages\seaborn\distributions.py:2551: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning)
On observe que les deux transformations donnent approximativement les mêmes résulats.
Je décide donc d'utiliser la transformation Yeo-Johnson.
Sauvegarde de DF avant Standardisation des données :
df_original = df.copy()
Après analyse je décide d'utiliser la transformation Yeo-Johnson
sur l'ensemble de mes features à l'exception de la feature 'total_of_orders'
pour laquelle j'utilise la transformation MinMaxScaler.
df[['late_delivery',
'payment_type_score',
'total_amout_ordered',
'total_amout_paid',
'average_basket',
'day_of_order',
'month_of_order',
'days_since_last_order',
'customer_satisfaction']] = PowerTransformer()\
.fit_transform(df[['late_delivery',
'payment_type_score',
'total_amout_ordered',
'total_amout_paid',
'average_basket',
'day_of_order',
'month_of_order',
'days_since_last_order',
'customer_satisfaction']])
df.total_of_orders = MinMaxScaler().fit_transform(df[['total_of_orders']])
Je recrée un DataFrame avec mes valeurs transformées :
df = pd.DataFrame(PowerTransformer()
.fit_transform(df.select_dtypes(exclude='object')),
columns=df.select_dtypes(exclude='object').columns)
df.head().T
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| late_delivery | -0.13328 | -0.48294 | -1.42513 | 0.09909 | -0.71296 |
| payment_type_score | 0.54211 | 0.54211 | 0.54211 | 0.54211 | 0.54211 |
| total_of_orders | -0.17588 | -0.17588 | -0.17588 | -0.17588 | -0.17588 |
| total_amout_ordered | 0.27325 | 0.99626 | 0.37629 | 0.43837 | 0.80865 |
| total_amout_paid | 0.24411 | 0.94853 | 0.31298 | 0.40136 | 0.72714 |
| average_basket | 0.30642 | 1.05961 | 0.41317 | 0.47758 | 0.86323 |
| day_of_order | -0.89629 | 0.71235 | 1.63212 | -0.89629 | 1.63212 |
| month_of_order | -0.25474 | -1.72527 | -0.25474 | -0.92350 | 0.35357 |
| days_since_last_order | 1.40975 | 0.06681 | -0.83503 | -0.32868 | -1.55286 |
| customer_satisfaction | -0.88653 | 0.83047 | 0.83047 | 0.83047 | 0.83047 |
Sauvegarde de DF après Standardisation des données :
df_std = df.copy()
N'ayant pas détecter de valeur aberrantes,
seulement des valeurs extrêmes que je juge pertinentes,
je décide de ne pas traiter les outliers.
# pickle.dump(df, open('df_val_original', 'wb'))
# pickle.dump(df_std, open('df_val_std', 'wb'))
# df = pickle.load(open('df_val_original', 'rb'))
# df_std = pickle.load(open('df_val_std', 'rb'))
Je vais maintenant segmenter mes clients.
Je décide de commencer pour une segmentation **RFM**.
La méthode de segmentation **RFM** permet un classement
des clients en fonction de leurs habitudes d'achat
défini par leur :
J'utiliserai différentes techniques de segmentation,
comme K-Means, DBScan ou encore Agglomerative Clustering
L'objectif sera d'obtenir des segmentations qui soient à la fois cohérentes
et exploitables pour les équipes marketing.
Pour cette raison, pour chaque segmentation client,
j'évaluerai les segmentations selon les critères suivants :
Je répèterai ces opérations en ajoutant des features
aux features RFM existantes, une à une, jusqu'à obtenir
une segmentation que je jugerai satisfaisante
d'un point de vue métier.
Features utilisées :
Création du DataFrame 'df3' contenant les valeurs
originales non standardisées, pour l'affichage
des graphiques BoxPlot.
df3 = df_original.copy()[['days_since_last_order',
'total_of_orders',
'total_amout_paid']]
Création du DataFrame 'df3std'
contenant les valeurs standardisées :
df3std = df_std[['days_since_last_order',
'total_of_orders',
'total_amout_paid']]
Je renomme les colonnes pour plus de lisibilité :
df3.rename(columns={'days_since_last_order': 'recency',
'total_of_orders': 'frequency',
'total_amout_paid': 'amount'},
inplace=True)
df3std.rename(columns={'days_since_last_order': 'recency',
'total_of_orders': 'frequency',
'total_amout_paid': 'amount'},
inplace=True)
Le coefficient de silhouette est une mesure de
qualité d'une partition d'un ensemble de données.
Pour chaque point, son coefficient de silhouette
est la différence entre la distance moyenne
avec les points du même groupe que lui (cohésion)
et la distance moyenne avec les points des autres
groupes voisins (séparation).
Si cette différence est négative,
le point est en moyenne plus proche du groupe voisin
que du sien et il est donc mal classé.
A l'inverse, si cette différence est positive,
le point est en moyenne plus proche de son groupe
que du groupe voisin et il est donc bien classé.
Le coefficient de silhouette proprement dit est
la moyenne du coefficient de silhouette pour tous les points.
J'utilise l'algorithme KMeans pour calculer le coefficient
de silhouette, pour différents nombres de clusters allant de 3 à 20.
Nous analyserons ensuite la valeurs des différents coefficients
de silhouette pour déterminer, à priori, le nombre optimal de clusters.
Nous utiliserons également la méthode du coude via la fonction KElbowVisualizer.
model = KMeans()
visualizer = KElbowVisualizer(model,
k=(3,21),
metric='silhouette',
timings=True)
visualizer.fit(df3std)
visualizer.poof()
<AxesSubplot:title={'center':'Silhouette Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='silhouette score'>
model = KMeans()
visualizer = KElbowVisualizer(model,
k=(2, 21))
visualizer.fit(df3std)
visualizer.poof()
<AxesSubplot:title={'center':'Distortion Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='distortion score'>
Les valeurs du coefficient de silhouette ne varient pas
significativement quel que soit la valeur de k.
Cependant, on peut noter que les valeurs de k
les plus optimisés sont 4, 7, 11 et 13.
Il peut être compliqué d'd'interpréter plus de 10 clusters,
et je vais donc me concentrer ici sur les valeurs de k valant 4 et 7,
plus utile pour une interprétation métier.
Affichons maintenant les différents clusters en fonction de leur population :
Le visualiseur de silhouette affiche le coefficient de silhouette pour chaque cluster,
en évaluant visuellement leur densité et leur séparation par rapport aux autres clusters.
Le score est calculé en faisant la moyenne du coefficient de silhouette
pour chaque échantillon par cluster, calculé comme la différence
entre la distance moyenne intra-cluster et la distance moyenne
au plus proche cluster pour chaque échantillon,
normalisée par la valeur maximale.
Cela donne un score entre -1 et +1, où les scores proches de +1 indiquent
une séparation élevée et les scores proches de -1 indiquent
que les échantillons peuvent avoir été affectés au mauvais cluster.
Dans les graphiques de SilhouetteVisualizer, les clusters ayant des scores
plus élevés ont des silhouettes plus larges, mais les grappes qui sont moins
cohésives n'atteignent pas le score moyen de toutes les clusters,
qui est représenté par une ligne verticale pointillée en rouge.
model = KMeans(4)
visualizer = SilhouetteVisualizer(model)
visualizer.fit(df3std) # Fit the data to the visualizer
visualizer.poof() # Draw/show/poof the data
<AxesSubplot:title={'center':'Silhouette Plot of KMeans Clustering for 93350 Samples in 4 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
3 des 4 clusters semblent très homogènes,
aussi bien en population qu'en fonction de leur coefficient de silhouette.
Un des clusters se démarque cependant par une population beaucoup plus faible
mais avec un coefficient de silhouette nettement plus élevé.
Ce groupe de client semble se démarquer nettement du reste de la population.
Enfin, aucun des clusters n'a d'échantillon avec un coefficient de silhouette inférieur à 0.
model = KMeans(7)
visualizer = SilhouetteVisualizer(model)
visualizer.fit(df3std) # Fit the data to the visualizer
visualizer.poof() # Draw/show/poof the data
<AxesSubplot:title={'center':'Silhouette Plot of KMeans Clustering for 93350 Samples in 7 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
Ici les différents clusters sont un peu moins homogènes qu'avec seulement 4 clusters.
Quelques échantillons appartenant à différents cluster ont un coefficient
de silhouette inférieur à 0 et peuvent aller jusqu'à -0.1.
On observe toujours notre cluster à faible population
et aux taux de coefficient de silhouette élevé.
A ce stade, avec 3 features, il me semble plus pertinent
de partitionner nos clients en 4 clusters.
Quel que soit le nombre de cluster choisi,
on remarque qu'un cluster reste identique.
Il contient moins de clients que n'importe quel autre cluster
mais possède une bonne valeur de coefficient de silhouette.
Utilisation de K-Means pour calculer 4, 7, 11 et 13 clusters,
les valeurs optimales en nombre de clusters d'après le coefficient de silhouette.
for i in [4,7,11,13]:
print('n_clusters =',i)
df3['cluster_number_for_'+str(i)+'_clusters'] =\
pd.DataFrame(data=KMeans(n_clusters=i).fit(df3std).labels_,
index=df3.index)
n_clusters = 4 n_clusters = 7 n_clusters = 11 n_clusters = 13
df3.head(3).T
| 0 | 1 | 2 | |
|---|---|---|---|
| recency | 470.00000 | 228.00000 | 101.00000 |
| frequency | 1.00000 | 1.00000 | 1.00000 |
| amount | 146.87000 | 335.48000 | 157.73000 |
| cluster_number_for_4_clusters | 0.00000 | 0.00000 | 1.00000 |
| cluster_number_for_7_clusters | 2.00000 | 5.00000 | 3.00000 |
| cluster_number_for_11_clusters | 8.00000 | 7.00000 | 3.00000 |
| cluster_number_for_13_clusters | 10.00000 | 11.00000 | 4.00000 |
J'applique une Analyse en Composante Principale à mon jeu de donnée
en le ramenant en 2 dimensions afin de pouvoir visualiser graphiquement
les différents clusters :
pca = PCA(n_components=2)
pca.fit(df3std)
print('Variance explained by the two principals components:',
pca.explained_variance_ratio_.cumsum()[-1])
X_pca = pca.transform(df3std)
Variance explained by the two principals components: 0.7437380791066505
Affichage des clusters en fonction du nombre total de clusters :
plt.figure(figsize=(12, 12))
nbColumns = 2
nbRows = len(df3.columns[3:])//nbColumns +\
(len(df3.columns[3:])%nbColumns > 0)
for i,j in enumerate(df3.columns[3:]):
# Extract the number of clusters
clusterNumber = int(re.sub('cluster_number_for_',
'',
re.sub('\_clusters$',
'',
j)))
# Plot Clusters
plt.subplot(nbRows,nbColumns,i+1)
plt.scatter(X_pca[:, 0],
X_pca[:, 1],
c=df3[j],
cmap="viridis")
plt.title(f'Number of cluster: {clusterNumber}')
print('-----------------------------')
print('Number of clusters:',clusterNumber)
for k,l in enumerate(df3[j].value_counts().sort_index()):
print(f'Cluster n°{k} -- number of customers: {l}')
print('-----------------------------')
----------------------------- Number of clusters: 4 Cluster n°0 -- number of customers: 29182 Cluster n°1 -- number of customers: 32288 Cluster n°2 -- number of customers: 2801 Cluster n°3 -- number of customers: 29079 ----------------------------- ----------------------------- Number of clusters: 7 Cluster n°0 -- number of customers: 14803 Cluster n°1 -- number of customers: 2801 Cluster n°2 -- number of customers: 14069 Cluster n°3 -- number of customers: 15579 Cluster n°4 -- number of customers: 13431 Cluster n°5 -- number of customers: 11256 Cluster n°6 -- number of customers: 21411 ----------------------------- ----------------------------- Number of clusters: 11 Cluster n°0 -- number of customers: 6684 Cluster n°1 -- number of customers: 7955 Cluster n°2 -- number of customers: 2801 Cluster n°3 -- number of customers: 9776 Cluster n°4 -- number of customers: 6993 Cluster n°5 -- number of customers: 11559 Cluster n°6 -- number of customers: 11039 Cluster n°7 -- number of customers: 12948 Cluster n°8 -- number of customers: 10609 Cluster n°9 -- number of customers: 6291 Cluster n°10 -- number of customers: 6695 ----------------------------- ----------------------------- Number of clusters: 13 Cluster n°0 -- number of customers: 5462 Cluster n°1 -- number of customers: 5595 Cluster n°2 -- number of customers: 2801 Cluster n°3 -- number of customers: 10756 Cluster n°4 -- number of customers: 9858 Cluster n°5 -- number of customers: 6662 Cluster n°6 -- number of customers: 5408 Cluster n°7 -- number of customers: 9621 Cluster n°8 -- number of customers: 9199 Cluster n°9 -- number of customers: 4814 Cluster n°10 -- number of customers: 8800 Cluster n°11 -- number of customers: 5942 Cluster n°12 -- number of customers: 8432 -----------------------------
Affichage du nombre de Client/Cluster en fonction du nombre de clusters total :
for i,j in enumerate(df3.iloc[:,3:]):
clusterNumber = int(re.sub('cluster_number_for_',
'',
re.sub('\_clusters$',
'',
j)))
plt.figure(figsize=(12,7))
plt.hist(df3[j],
range=(0, clusterNumber),
bins=clusterNumber,
color='yellow',
edgecolor = 'red',
align='left')
plt.title(j)
plt.xlabel('Cluster Number')
plt.ylabel('Number of Custumer')
J'affiche pour chaque feature,
les différents clusters sous forme de boxplot
en fonction du nombre de clusters total.
Cette visualisation doit nous permetre de comprendre
et d'interpeter les profils des différents clients.
for i,j in enumerate(df3.iloc[:,3:]):
clusterNumber = int(re.sub('cluster_number_for_',
'',
re.sub('\_clusters$',
'',
j)))
plt.figure(figsize=(20,10))
for k in range(3):
plt.subplot(1,3,k+1)
plt.title(f'Number of cluster: {clusterNumber} - Feature: {df3.columns[k]}')
sns.boxplot(x=df3.iloc[:,i+3],
y=df3.iloc[:,k],
showfliers = False,
showmeans=True)
Interpretation lorsque le jeu de donnée est divisé en :
Le diagramme de Kiviat nous permet de visualiser rapidement
et efficacement le profil client de chaque cluster.
Le graphique affiche les valeurs moyennes, normalisés entre 0 et 1
de chaque feature, pour chaque cluster.
Cela permet de déterminer d'un seul coup le profil type
de chaque groupe de client que KMeans a identifié.
df3_kiviat = df3.copy()
df3_kiviat[['recency',
'frequency',
'amount']] = MinMaxScaler()\
.fit_transform(PowerTransformer()\
.fit_transform(df3_kiviat[['recency',
'frequency',
'amount']]))
Je ne conserve que le nombre de *clusters optimal*
pour l'affichage des **Diagrammes de Kiviat** :
df3_kiviat = df3_kiviat[['recency',
'frequency',
'amount',
'cluster_number_for_4_clusters']]
plt.figure(figsize=(20,10))
for k in range(3):
plt.subplot(1,3,k+1)
plt.title(f'Cluster number: 4 - Feature: {df3_kiviat.columns[k]}')
sns.boxplot(x=df3_kiviat.iloc[:,-1],
y=df3_kiviat.iloc[:,k],
showfliers = False,
showmeans=True)
nbColumns = 2
nbRows = df3_kiviat.iloc[:,-1].nunique()//nbColumns +\
(df3_kiviat.iloc[:,-1].nunique()%nbColumns > 0)
colClusters = df3_kiviat.columns[-1]
plt.figure(figsize=(15,5*nbRows))
for i in sorted(df3_kiviat.iloc[:,-1].unique()):
# number of variable
categories=list(df3_kiviat)[:-1]
N = len(categories)
# We are going to plot the first line of the data frame.
# But we need to repeat the first value to close the circular graph:
values=df3_kiviat[df3_kiviat[colClusters] == i]\
.drop(colClusters, axis=1)\
.mean().values.tolist()
values += values[:1]
# What will be the angle of each axis in the plot?
# (we divide the plot / number of variable)
angles = [n / float(N) * 2 * pi for n in range(N)]
angles += angles[:1]
# Initialise the spider plot
ax = plt.subplot(nbRows,nbColumns,i+1, polar=True)
# Draw one axe per variable + add labels labels yet
plt.xticks(angles[:-1],
categories,
color='grey',
size=8,
ha='left')
# Draw ylabels
ax.set_rlabel_position(0)
plt.yticks([0,0.5,1], ["0","0.5","1"],
color="grey",
size=7)
plt.ylim(0,1)
# Plot data
ax.plot(angles,
values,
linewidth=1,
linestyle='solid')
# Fill area
ax.fill(angles,
values,
'b',
alpha=0.1)
plt.title(f'Cluster n°{i}')
DBSCAN est un algorithme de clustering qui s’appuie sur la densité
estimée des clusters pour effectuer le partitionnement.
L'algorithme DBSCAN utilise 2 paramètres :
Les paramètres d'entrées sont donc une estimation de la densité
de points des clusters.
L'idée de base de l'algorithme est ensuite, pour un point donné,
de récupérer son epsilon-voisinage et de vérifier qu'il contient bien
MinPts points ou plus.
Ce point est alors considéré comme faisant partie d'un cluster.
On parcourt ensuite l'epsilon-voisinage de proche en proche afin
de trouver l'ensemble des points du cluster.
Contrairement à KMeans, ici nous ne choisissons pas
à l'avance le nombre de Cluster.
Suppression des colonnes contenant les clusters K-Means dans **df3std** et **df3** :
df3std = df3std.iloc[:,:3]
df3 = df3.iloc[:,:3]
Test de l'algorithme **DBSCAN** pour **EPS allant de 0.1 à 1.5** par pas de **0.1** :
coeffSilhouette = []
numberOfCluster = []
for i in range(1,16):
print('eps =',i/10)
db3=DBSCAN(eps=i/10, min_samples=10,).fit(df3std)
# Number of clusters in labels, ignoring noise if present.
n_clusters_ = len(set(db3.labels_)) - (1 if -1 in db3.labels_ else 0)
coeffSilhouette.append(silhouette_score(df3std, db3.labels_))
numberOfCluster.append(n_clusters_)
# I assign the calculated labels in a new column of the DataFrame df3
# for future analysis of the number of customers per cluster.
df3['cluster_for_EPS_'+str(i/10)] =\
pd.DataFrame(data=db3.labels_,
index=df3.index)
eps = 0.1 eps = 0.2 eps = 0.3 eps = 0.4 eps = 0.5 eps = 0.6 eps = 0.7 eps = 0.8 eps = 0.9 eps = 1.0 eps = 1.1 eps = 1.2 eps = 1.3 eps = 1.4 eps = 1.5
# Saving the coeffSilhouette and numberOfCluster Lists
pickle.dump(coeffSilhouette, open('dbscan_coeffSilhouette_3f', 'wb'))
pickle.dump(numberOfCluster, open('dbscan_numberOfCluster_3f', 'wb'))
# Restoration of the coeffSilhouette and numberOfCluster lists
# coeffSilhouette = pickle.load(open('dbscan_coeffSilhouette_3f', 'rb'))
# numberOfCluster = pickle.load(open('dbscan_numberOfCluster_3f', 'rb'))
Pour chaque **EPS** testé, affichage du nombre de cluster et du coefficient de silhouette :
plt.figure(figsize=(12,12))
plt.subplot(211)
plt.plot(list(map(str, np.linspace(1,15,15,dtype='int')/10)), coeffSilhouette)
plt.grid()
plt.xlabel('EPS')
plt.ylabel('Silhouette Coefficient')
plt.title('Silhouette coefficient according to EPS value')
plt.subplot(212)
plt.bar(list(map(str, np.linspace(1,15,15,dtype='int')/10)), numberOfCluster)
plt.grid()
plt.xlabel('EPS')
plt.ylabel('Number of Clusters')
plt.title('Number of Clusters according to EPS value')
Text(0.5, 1.0, 'Number of Clusters according to EPS value')
Affichage du nombre de clients / cluster en fonction de **EPS** :
for i,j in enumerate(df3.columns[3:]):
print('-----------------------------')
print('Number of Clusters for EPS =',
(i+1)/10,
':',
df3[j].nunique())
for index, value in df3[j].value_counts().sort_index().items():
print(f'Cluster n°{index} -- Number of customers: {value}')
print('-----------------------------')
----------------------------- Number of Clusters for EPS = 0.1 : 22 Cluster n°-1 -- Number of customers: 828 Cluster n°0 -- Number of customers: 89977 Cluster n°1 -- Number of customers: 10 Cluster n°2 -- Number of customers: 175 Cluster n°3 -- Number of customers: 1929 Cluster n°4 -- Number of customers: 15 Cluster n°5 -- Number of customers: 14 Cluster n°6 -- Number of customers: 205 Cluster n°7 -- Number of customers: 35 Cluster n°8 -- Number of customers: 18 Cluster n°9 -- Number of customers: 11 Cluster n°10 -- Number of customers: 8 Cluster n°11 -- Number of customers: 28 Cluster n°12 -- Number of customers: 11 Cluster n°13 -- Number of customers: 11 Cluster n°14 -- Number of customers: 10 Cluster n°15 -- Number of customers: 10 Cluster n°16 -- Number of customers: 14 Cluster n°17 -- Number of customers: 11 Cluster n°18 -- Number of customers: 10 Cluster n°19 -- Number of customers: 10 Cluster n°20 -- Number of customers: 10 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.2 : 4 Cluster n°-1 -- Number of customers: 139 Cluster n°0 -- Number of customers: 90242 Cluster n°1 -- Number of customers: 240 Cluster n°2 -- Number of customers: 2729 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.3 : 4 Cluster n°-1 -- Number of customers: 35 Cluster n°0 -- Number of customers: 90290 Cluster n°1 -- Number of customers: 243 Cluster n°2 -- Number of customers: 2782 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.4 : 3 Cluster n°-1 -- Number of customers: 14 Cluster n°0 -- Number of customers: 90544 Cluster n°1 -- Number of customers: 2792 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.5 : 3 Cluster n°-1 -- Number of customers: 6 Cluster n°0 -- Number of customers: 90547 Cluster n°1 -- Number of customers: 2797 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.6 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.7 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.8 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.9 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.0 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.1 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.2 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.3 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.4 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.5 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 -----------------------------
L'utilisation de DBSCAN dans notre cas est inexploitable
pour une utilisation métier car les clusters ne sont pas équilibrés.
Nous avons par exemple des clusters contenants une dizaine de clients
alors qu'un autre cluster en contient 89977.
Le dendrogramme est une arborescence qui affiche les groupes formés
par le regroupement des observations à chaque étape et leurs niveaux de similarité.
Le niveau de similarité est mesuré le long de l'axe vertical
et les différentes observations sont répertoriées le long de l'axe horizontal.
# setting distance_threshold=0 ensures we compute the full tree.
model = AgglomerativeClustering(distance_threshold=0, n_clusters=None)
# I select my data
X = df3std.iloc[:10000,:]
model = model.fit(X)
plt.figure(figsize=(10,10))
plt.title('Hierarchical Clustering Dendrogram')
# plot the top three levels of the dendrogram
plot_dendrogram(model, truncate_mode='level', p=3)
plt.xlabel("Number of points in node (or index of point if no parenthesis).")
plt.xticks(rotation=45)
plt.show()
Plusieurs remarques sont à prendre en compte sur l'utilisation de cette méthode :
Features utilisées :
Création du DataFrame 'df4' contenant les valeurs
originales non standardisées, pour l'affichage
des graphiques BoxPlot.
df4 = df_original.copy()[['days_since_last_order',
'total_of_orders',
'total_amout_paid',
'customer_satisfaction']]
Création du DataFrame 'df4std'
contenant les valeurs standardisées :
df4std = df_std[['days_since_last_order',
'total_of_orders',
'total_amout_paid',
'customer_satisfaction']]
Je renomme les colonnes pour plus de lisibilité :
df4.rename(columns={'days_since_last_order': 'recency',
'total_of_orders': 'frequency',
'total_amout_paid': 'amount',
'customer_satisfaction': 'satisfaction'},
inplace=True)
df4std.rename(columns={'days_since_last_order': 'recency',
'total_of_orders': 'frequency',
'total_amout_paid': 'amount',
'customer_satisfaction': 'satisfaction'},
inplace=True)
Le coefficient de silhouette est une mesure de
qualité d'une partition d'un ensemble de données.
Pour chaque point, son coefficient de silhouette
est la différence entre la distance moyenne
avec les points du même groupe que lui (cohésion)
et la distance moyenne avec les points des autres
groupes voisins (séparation).
Si cette différence est négative,
le point est en moyenne plus proche du groupe voisin
que du sien et il est donc mal classé.
A l'inverse, si cette différence est positive,
le point est en moyenne plus proche de son groupe
que du groupe voisin et il est donc bien classé.
Le coefficient de silhouette proprement dit est
la moyenne du coefficient de silhouette pour tous les points.
J'utilise l'algorithme KMeans pour calculer le coefficient
de silhouette, pour différents nombres de clusters allant de 3 à 20.
Nous analyserons ensuite la valeur des différents coefficient
de silhouette pour déterminer, à priori, le nombre optimal de cluster.
Nous utiliserons également la méthode du coude via la fonction KElbowVisualizer.
model = KMeans()
visualizer = KElbowVisualizer(model,
k=(3,21),
metric='silhouette',
timings=True)
visualizer.fit(df4std)
visualizer.poof()
<AxesSubplot:title={'center':'Silhouette Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='silhouette score'>
model = KMeans()
visualizer = KElbowVisualizer(model,
k=(2, 21))
visualizer.fit(df4std)
visualizer.poof()
<AxesSubplot:title={'center':'Distortion Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='distortion score'>
Les valeurs du coefficient de silhouette ne varient pas
significativement quel que soit la valeur de k.
Cependant, on peut noter que les valeurs de k
les plus optimisés sont 3 et 7.
Affichons maintenant les différents clusters en fonction de leur population :
Le visualiseur de silhouette affiche le coefficient de silhouette pour chaque cluster,
en évaluant visuellement leur densité et leur séparation par rapport aux autres clusters.
Le score est calculé en faisant la moyenne du coefficient de silhouette
pour chaque échantillon par cluster, calculé comme la différence
entre la distance moyenne intra-cluster et la distance moyenne
au plus proche cluster pour chaque échantillon,
normalisée par la valeur maximale.
Cela donne un score entre -1 et +1, où les scores proches de +1 indiquent
une séparation élevée et les scores proches de -1 indiquent
que les échantillons peuvent avoir été affectés au mauvais cluster.
Dans les graphiques de SilhouetteVisualizer, les clusters ayant des scores
plus élevés ont des silhouettes plus larges, mais les grappes qui sont moins
cohésives n'atteignent pas le score moyen de toutes les cluster,
qui est représenté par une ligne verticale pointillée en rouge.
model = KMeans(3)
visualizer = SilhouetteVisualizer(model)
visualizer.fit(df4std) # Fit the data to the visualizer
visualizer.poof() # Draw/show/poof the data
<AxesSubplot:title={'center':'Silhouette Plot of KMeans Clustering for 93350 Samples in 3 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
3 des 4 clusters semblent assez homogènes,
aussi bien en population qu'en fonction de leur coefficient de silhouette.
Un des clusters se démarque cependant par une population beaucoup plus faible
mais avec un coefficient de silhouette nettement plus élevé.
Ce groupe de client semble se démarquer nettement du reste de la population.
Enfin, aucun des clusters n'a d'échantillon avec un coefficient de silhouette inférieur à 0.
model = KMeans(7)
visualizer = SilhouetteVisualizer(model)
visualizer.fit(df4std) # Fit the data to the visualizer
visualizer.poof() # Draw/show/poof the data
<AxesSubplot:title={'center':'Silhouette Plot of KMeans Clustering for 93350 Samples in 7 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
Ici aussi les différents clusters semblent assez homogènes.
Quelques échantillons appartenant à différents clusters ont un coefficient
de silhouette inférieur à 0 et ne semble pas aller au-delà de -0.05.
On observe toujours notre cluster à faible population
et aux taux de coefficient de silhouette élevé.
A ce stade, avec 4 features, il m'est difficile de déterminer
le nombre de clusters optimal autrement que par le score
de silhouette globale qui favorise une clusterisation à 3 clusters.
Quel que soit le nombre de clusters choisi,
on remarque qu'un cluster reste identique.
Il contient moins de clients que n'importe quel autre cluster
mais possède une bonne valeur de coefficient de silhouette.
Utilisation de K-Means pour calculer 3 et 7 clusters,
les valeurs optimales en nombre de clusters d'après
le coefficient de silhouette.
for i in [3, 7]:
print('n_clusters =', i)
df4['cluster_number_for_'+str(i)+'_clusters'] =\
pd.DataFrame(data=KMeans(n_clusters=i).fit(df4std).labels_,
index=df4.index)
n_clusters = 3 n_clusters = 7
df4.head(3).T
| 0 | 1 | 2 | |
|---|---|---|---|
| recency | 470.00000 | 228.00000 | 101.00000 |
| frequency | 1.00000 | 1.00000 | 1.00000 |
| amount | 146.87000 | 335.48000 | 157.73000 |
| satisfaction | 4.00000 | 5.00000 | 5.00000 |
| cluster_number_for_3_clusters | 0.00000 | 1.00000 | 1.00000 |
| cluster_number_for_7_clusters | 1.00000 | 6.00000 | 2.00000 |
J'applique une Analyse en Composante Principale à mon jeu de donnée
en le ramenant en 2 dimensions afin de pouvoir visualiser graphiquement
les différents clusters :
pca = PCA(n_components=2)
pca.fit(df4std)
print('Variance explained by the two principals components:',
pca.explained_variance_ratio_.cumsum()[-1])
X_pca = pca.transform(df4std)
Variance explained by the two principals components: 0.5679170405300833
Affichage des clusters en fonction du nombre total de clusters :
plt.figure(figsize=(20, 10))
nbColumns = 1
nbRows = len(df4.columns[4:])//nbColumns +\
(len(df4.columns[4:])%nbColumns > 0)
for i,j in enumerate(df4.columns[4:]):
# Extract the number of clusters
clusterNumber = int(re.sub('cluster_number_for_',
'',
re.sub('\_clusters$',
'',
j)))
# Plot Clusters
plt.subplot(nbRows,nbColumns,i+1)
plt.scatter(X_pca[:, 0],
X_pca[:, 1],
c=df4[j],
cmap="viridis")
plt.title(f'Number of cluster: {clusterNumber}')
print('-----------------------------')
print('Number of clusters:',clusterNumber)
for k,l in enumerate(df4[j].value_counts().sort_index()):
print(f'Cluster n°{k} -- number of customers: {l}')
print('-----------------------------')
----------------------------- Number of clusters: 3 Cluster n°0 -- number of customers: 37454 Cluster n°1 -- number of customers: 53095 Cluster n°2 -- number of customers: 2801 ----------------------------- ----------------------------- Number of clusters: 7 Cluster n°0 -- number of customers: 15868 Cluster n°1 -- number of customers: 12734 Cluster n°2 -- number of customers: 19582 Cluster n°3 -- number of customers: 2801 Cluster n°4 -- number of customers: 12886 Cluster n°5 -- number of customers: 11825 Cluster n°6 -- number of customers: 17654 -----------------------------
Affichage du nombre de Client/Cluster en fonction du nombre de cluster total :
for i,j in enumerate(df4.iloc[:,4:]):
clusterNumber = int(re.sub('cluster_number_for_',
'',
re.sub('\_clusters$',
'',
j)))
plt.figure(figsize=(12,7))
plt.hist(df4[j],
range=(0, clusterNumber),
bins=clusterNumber,
color='yellow',
edgecolor = 'red',
align='left')
plt.title(j)
plt.xlabel('Cluster Number')
plt.ylabel('Number of Custumer')
J'affiche pour chaque feature,
les différents clusters sous forme de boxplot
en fonction du nombre de clusters total.
Cette visualisation doit nous permettre de comprendre
et d'interpréter les profils des différents clients.
for i,j in enumerate(df4.iloc[:,4:]):
clusterNumber = int(re.sub('cluster_number_for_',
'',
re.sub('\_clusters$',
'',
j)))
plt.figure(figsize=(20,10))
for k in range(4):
plt.subplot(1,4,k+1)
plt.title(f'Number of cluster: {clusterNumber} - Feature: {df4.columns[k]}')
sns.boxplot(x=df4.iloc[:,i+4],
y=df4.iloc[:,k],
showfliers = False,
showmeans=True)
Interprétation lorsque le jeu de donnée est divisé en :
Le diagramme de Kiviat nous permet de visualiser rapidement
et efficacement le profil client de chaque cluster.
Le graphique affiche les valeurs moyennes, normalisés entre 0 et 1
de chaque features, pour chaque cluster.
Cela permet de déterminer d'un seul coup le profil type
de chaque groupe de client que KMeans a identifié.
df4_kiviat = df4.copy()
df4_kiviat[['recency',
'frequency',
'amount',
'satisfaction']] =\
MinMaxScaler().fit_transform(PowerTransformer()
.fit_transform(df4_kiviat[['recency',
'frequency',
'amount',
'satisfaction']]))
Je ne conserve que le nombre de *clusters optimal*
pour l'affichage des **Diagramme de Kiviat** :
df4_kiviat = df4_kiviat[['recency',
'frequency',
'amount',
'satisfaction',
'cluster_number_for_7_clusters']]
plt.figure(figsize=(20, 10))
for k in range(4):
plt.subplot(1, 4, k+1)
plt.title(f'Cluster number: 7 - Feature: {df4_kiviat.columns[k]}')
sns.boxplot(x=df4_kiviat.iloc[:, -1],
y=df4_kiviat.iloc[:, k],
showfliers=False,
showmeans=True)
nbColumns = 2
nbRows = df4_kiviat.iloc[:, -1].nunique()//nbColumns +\
(df4_kiviat.iloc[:, -1].nunique() % nbColumns > 0)
colClusters = df4_kiviat.columns[-1]
plt.figure(figsize=(7*nbColumns, 7*nbRows))
for i in sorted(df4_kiviat.iloc[:, -1].unique()):
# number of variable
categories = list(df4_kiviat)[:-1]
N = len(categories)
# We are going to plot the first line of the data frame.
# But we need to repeat the first value to close the circular graph:
values = df4_kiviat[df4_kiviat[colClusters] == i]\
.drop(colClusters, axis=1)\
.mean().values.tolist()
values += values[:1]
# values
# What will be the angle of each axis in the plot?
# (we divide the plot / number of variable)
angles = [n / float(N) * 2 * pi for n in range(N)]
angles += angles[:1]
# Initialise the spider plot
ax = plt.subplot(nbRows, nbColumns, i+1, polar=True)
# Draw one axe per variable + add labels labels yet
plt.xticks(angles[:-1],
categories,
color='grey',
size=8,
ha='left')
# Draw ylabels
ax.set_rlabel_position(0)
plt.yticks([0, 0.5, 1], ["0", "0.5", "1"],
color="grey",
size=7)
plt.ylim(0, 1)
# Plot data
ax.plot(angles,
values,
linewidth=1,
linestyle='solid')
# Fill area
ax.fill(angles,
values,
'b',
alpha=0.1)
plt.title(f'Cluster n°{i}')
DBSCAN est un algorithme de clustering qui s’appuie sur la densité
estimée des clusters pour effectuer le partitionnement.
L'algorithme DBSCAN utilise 2 paramètres :
Les paramètres d'entrées sont donc une estimation de la densité
de points des clusters.
L'idée de base de l'algorithme est ensuite, pour un point donné,
de récupérer son epsilon-voisinage et de vérifier qu'il contient bien
MinPts points ou plus.
Ce point est alors considéré comme faisant partie d'un cluster.
On parcourt ensuite l'epsilon-voisinage de proche en proche afin
de trouver l'ensemble des points du cluster.
Contrairement à KMeans, ici nous ne choisissons pas
à l'avance le nombre de Clusters.
Suppression des colonnes contenant les clusters K-Means dans **df4std** et **df4** :
df4std = df4std.iloc[:,:4]
df4 = df4.iloc[:,:4]
Test de l'algorithme DBSCAN pour **EPS allant de 0.1 à 1.5** par pas de **0.1** :
coeffSilhouette = []
numberOfCluster = []
for i in range(1,16):
print('eps =',i/10)
db4=DBSCAN(eps=i/10, min_samples=10,).fit(df4std)
# Number of clusters in labels, ignoring noise if present.
n_clusters_ = len(set(db4.labels_)) - (1 if -1 in db4.labels_ else 0)
coeffSilhouette.append(silhouette_score(df4std, db4.labels_))
numberOfCluster.append(n_clusters_)
# I assign the calculated labels in a new column of the DataFrame df4
# for future analysis of the number of customers per cluster.
df4['cluster_for_EPS_'+str(i/10)] =\
pd.DataFrame(data=db4.labels_,
index=df4.index)
eps = 0.1 eps = 0.2 eps = 0.3 eps = 0.4 eps = 0.5 eps = 0.6 eps = 0.7 eps = 0.8 eps = 0.9 eps = 1.0 eps = 1.1 eps = 1.2 eps = 1.3 eps = 1.4 eps = 1.5
# Saving the coeffSilhouette and numberOfCluster Lists
pickle.dump(coeffSilhouette, open('dbscan_coeffSilhouette_4f', 'wb'))
pickle.dump(numberOfCluster, open('dbscan_numberOfCluster_4f', 'wb'))
# Restoration of the coeffSilhouette and numberOfCluster lists
# coeffSilhouette = pickle.load(open('dbscan_coeffSilhouette_4f', 'rb'))
# numberOfCluster = pickle.load(open('dbscan_numberOfCluster_4f', 'rb'))
Pour chaque EPS testé, affichage du nombre de cluster et du coefficient de silhouette :
plt.figure(figsize=(12,12))
plt.subplot(211)
plt.plot(list(map(str, np.linspace(1,15,15,dtype='int')/10)), coeffSilhouette)
plt.grid()
plt.xlabel('EPS')
plt.ylabel('Silhouette Coefficient')
plt.title('Silhouette coefficient according to EPS value')
plt.subplot(212)
plt.bar(list(map(str, np.linspace(1,15,15,dtype='int')/10)), numberOfCluster)
plt.grid()
plt.xlabel('EPS')
plt.ylabel('Number of Clusters')
plt.title('Number of Clusters according to EPS value')
plt.show()
Affichage du nombre de clients / cluster en fonction de **EPS** :
for i,j in enumerate(df4.columns[4:]):
print('-----------------------------')
print('Number of Clusters for EPS =',
(i+1)/10,
':',
df4[j].nunique())
for index, value in df4[j].value_counts().sort_index().items():
print(f'Cluster n°{index} -- Number of customers: {value}')
print('-----------------------------')
----------------------------- Number of Clusters for EPS = 0.1 : 73 Cluster n°-1 -- Number of customers: 4794 Cluster n°0 -- Number of customers: 17222 Cluster n°1 -- Number of customers: 52522 Cluster n°2 -- Number of customers: 17377 Cluster n°3 -- Number of customers: 18 Cluster n°4 -- Number of customers: 12 Cluster n°5 -- Number of customers: 14 Cluster n°6 -- Number of customers: 16 Cluster n°7 -- Number of customers: 18 Cluster n°8 -- Number of customers: 6 Cluster n°9 -- Number of customers: 15 Cluster n°10 -- Number of customers: 31 Cluster n°11 -- Number of customers: 16 Cluster n°12 -- Number of customers: 45 Cluster n°13 -- Number of customers: 39 Cluster n°14 -- Number of customers: 19 Cluster n°15 -- Number of customers: 13 Cluster n°16 -- Number of customers: 24 Cluster n°17 -- Number of customers: 18 Cluster n°18 -- Number of customers: 12 Cluster n°19 -- Number of customers: 14 Cluster n°20 -- Number of customers: 19 Cluster n°21 -- Number of customers: 14 Cluster n°22 -- Number of customers: 13 Cluster n°23 -- Number of customers: 420 Cluster n°24 -- Number of customers: 10 Cluster n°25 -- Number of customers: 17 Cluster n°26 -- Number of customers: 21 Cluster n°27 -- Number of customers: 21 Cluster n°28 -- Number of customers: 41 Cluster n°29 -- Number of customers: 15 Cluster n°30 -- Number of customers: 15 Cluster n°31 -- Number of customers: 8 Cluster n°32 -- Number of customers: 13 Cluster n°33 -- Number of customers: 19 Cluster n°34 -- Number of customers: 14 Cluster n°35 -- Number of customers: 17 Cluster n°36 -- Number of customers: 15 Cluster n°37 -- Number of customers: 13 Cluster n°38 -- Number of customers: 14 Cluster n°39 -- Number of customers: 12 Cluster n°40 -- Number of customers: 19 Cluster n°41 -- Number of customers: 36 Cluster n°42 -- Number of customers: 10 Cluster n°43 -- Number of customers: 31 Cluster n°44 -- Number of customers: 14 Cluster n°45 -- Number of customers: 12 Cluster n°46 -- Number of customers: 14 Cluster n°47 -- Number of customers: 8 Cluster n°48 -- Number of customers: 8 Cluster n°49 -- Number of customers: 15 Cluster n°50 -- Number of customers: 8 Cluster n°51 -- Number of customers: 11 Cluster n°52 -- Number of customers: 16 Cluster n°53 -- Number of customers: 9 Cluster n°54 -- Number of customers: 10 Cluster n°55 -- Number of customers: 7 Cluster n°56 -- Number of customers: 10 Cluster n°57 -- Number of customers: 7 Cluster n°58 -- Number of customers: 10 Cluster n°59 -- Number of customers: 7 Cluster n°60 -- Number of customers: 8 Cluster n°61 -- Number of customers: 10 Cluster n°62 -- Number of customers: 11 Cluster n°63 -- Number of customers: 10 Cluster n°64 -- Number of customers: 10 Cluster n°65 -- Number of customers: 10 Cluster n°66 -- Number of customers: 8 Cluster n°67 -- Number of customers: 10 Cluster n°68 -- Number of customers: 9 Cluster n°69 -- Number of customers: 8 Cluster n°70 -- Number of customers: 7 Cluster n°71 -- Number of customers: 11 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.2 : 24 Cluster n°-1 -- Number of customers: 1266 Cluster n°0 -- Number of customers: 17753 Cluster n°1 -- Number of customers: 52922 Cluster n°2 -- Number of customers: 19331 Cluster n°3 -- Number of customers: 124 Cluster n°4 -- Number of customers: 328 Cluster n°5 -- Number of customers: 14 Cluster n°6 -- Number of customers: 10 Cluster n°7 -- Number of customers: 21 Cluster n°8 -- Number of customers: 1236 Cluster n°9 -- Number of customers: 51 Cluster n°10 -- Number of customers: 56 Cluster n°11 -- Number of customers: 14 Cluster n°12 -- Number of customers: 49 Cluster n°13 -- Number of customers: 47 Cluster n°14 -- Number of customers: 8 Cluster n°15 -- Number of customers: 25 Cluster n°16 -- Number of customers: 10 Cluster n°17 -- Number of customers: 24 Cluster n°18 -- Number of customers: 14 Cluster n°19 -- Number of customers: 10 Cluster n°20 -- Number of customers: 14 Cluster n°21 -- Number of customers: 12 Cluster n°22 -- Number of customers: 11 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.3 : 8 Cluster n°-1 -- Number of customers: 357 Cluster n°0 -- Number of customers: 37275 Cluster n°1 -- Number of customers: 52944 Cluster n°2 -- Number of customers: 132 Cluster n°3 -- Number of customers: 1255 Cluster n°4 -- Number of customers: 26 Cluster n°5 -- Number of customers: 37 Cluster n°6 -- Number of customers: 1324 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.4 : 9 Cluster n°-1 -- Number of customers: 117 Cluster n°0 -- Number of customers: 37309 Cluster n°1 -- Number of customers: 52954 Cluster n°2 -- Number of customers: 132 Cluster n°3 -- Number of customers: 1399 Cluster n°4 -- Number of customers: 28 Cluster n°5 -- Number of customers: 60 Cluster n°6 -- Number of customers: 1338 Cluster n°7 -- Number of customers: 13 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.5 : 5 Cluster n°-1 -- Number of customers: 44 Cluster n°0 -- Number of customers: 37436 Cluster n°1 -- Number of customers: 53092 Cluster n°2 -- Number of customers: 1433 Cluster n°3 -- Number of customers: 1345 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.6 : 4 Cluster n°-1 -- Number of customers: 9 Cluster n°0 -- Number of customers: 37452 Cluster n°1 -- Number of customers: 53094 Cluster n°2 -- Number of customers: 2795 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.7 : 4 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 37453 Cluster n°1 -- Number of customers: 53095 Cluster n°2 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.8 : 4 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 37453 Cluster n°1 -- Number of customers: 53095 Cluster n°2 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.9 : 4 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 37453 Cluster n°1 -- Number of customers: 53095 Cluster n°2 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.0 : 4 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 37453 Cluster n°1 -- Number of customers: 53095 Cluster n°2 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.1 : 4 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 37453 Cluster n°1 -- Number of customers: 53095 Cluster n°2 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.2 : 4 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 37453 Cluster n°1 -- Number of customers: 53095 Cluster n°2 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.3 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.4 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.5 : 3 Cluster n°-1 -- Number of customers: 1 Cluster n°0 -- Number of customers: 90548 Cluster n°1 -- Number of customers: 2801 -----------------------------
L'utilisation de DBSCAN dans notre cas est inexploitable
pour une utilisation métier car les clusters ne sont pas équilibrés.
Nous avons par exemple des clusters contenants une dizaine de clients
alors qu'un autre cluster en contient 52522.
Le dendrogramme est une arborescence qui affiche les groupes formés
par le regroupement des observations à chaque étape et leurs niveaux de similarité.
Le niveau de similarité est mesuré le long de l'axe vertical
et les différentes observations sont répertoriées le long de l'axe horizontal.
# setting distance_threshold=0 ensures we compute the full tree.
model = AgglomerativeClustering(distance_threshold=0, n_clusters=None)
# I select my data
X = df4std.iloc[:10000,:]
model = model.fit(X)
plt.figure(figsize=(10,10))
plt.title('Hierarchical Clustering Dendrogram')
# plot the top three levels of the dendrogram
plot_dendrogram(model, truncate_mode='level', p=3)
plt.xlabel("Number of points in node (or index of point if no parenthesis).")
plt.xticks(rotation=45)
plt.show()
Plusieurs remarques sont à prendre en compte sur l'utilisation de cette méthode :
Features utilisées :
Création du DataFrame 'df5' contenant les valeurs
originales non standardisées, pour l'affichage
des graphiques BoxPlot.
df5 = df_original.copy()[['days_since_last_order',
'total_of_orders',
'total_amout_paid',
'customer_satisfaction',
'late_delivery']]
Création du DataFrame 'df5std'
contenant les valeurs standardisées :
df5std = df_std[['days_since_last_order',
'total_of_orders',
'total_amout_paid',
'customer_satisfaction',
'late_delivery']]
Je renomme les colonnes pour plus de lisibilité :
df5.rename(columns={'days_since_last_order': 'recency',
'total_of_orders': 'frequency',
'total_amout_paid': 'amount',
'customer_satisfaction': 'satisfaction',
'late_delivery': 'delivery'},
inplace=True)
df5std.rename(columns={'days_since_last_order': 'recency',
'total_of_orders': 'frequency',
'total_amout_paid': 'amount',
'customer_satisfaction': 'satisfaction',
'late_delivery': 'delivery'},
inplace=True)
Le coefficient de silhouette est une mesure de
qualité d'une partition d'un ensemble de données.
Pour chaque point, son coefficient de silhouette
est la différence entre la distance moyenne
avec les points du même groupe que lui (cohésion)
et la distance moyenne avec les points des autres
groupes voisins (séparation).
Si cette différence est négative,
le point est en moyenne plus proche du groupe voisin
que du sien et il est donc mal classé.
A l'inverse, si cette différence est positive,
le point est en moyenne plus proche de son groupe
que du groupe voisin et il est donc bien classé.
Le coefficient de silhouette proprement dit est
la moyenne du coefficient de silhouette pour tous les points.
J'utilise l'algorithme KMeans pour calculer le coefficient
de silhouette, pour différents nombres de clusters allant de 3 à 20.
Nous analyserons ensuite la valeurs des différents coefficients
de silhouette pour déterminer, à priori, le nombre optimal de clusters.
Nous utiliserons également la méthode du coude via la fonction KElbowVisualizer.
model = KMeans()
visualizer = KElbowVisualizer(model,
k=(3,21),
metric='silhouette',
timings=True)
visualizer.fit(df5std)
visualizer.poof()
<AxesSubplot:title={'center':'Silhouette Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='silhouette score'>
model = KMeans()
visualizer = KElbowVisualizer(model,
k=(3, 21))
visualizer.fit(df5std)
visualizer.poof()
<AxesSubplot:title={'center':'Distortion Score Elbow for KMeans Clustering'}, xlabel='k', ylabel='distortion score'>
Les valeurs du coefficient de silhouette ne varient pas
significativement quel que soit la valeur de k.
Cependant, on peut noter que les valeurs de k
les plus optimisés sont 3 et 9.
Affichons maintenant les différents clusters en fonction de leur population :
Le visualiseur de silhouette affiche le coefficient de silhouette pour chaque cluster,
en évaluant visuellement leur densité et leur séparation par rapport aux autres clusters.
Le score est calculé en faisant la moyenne du coefficient de silhouette
pour chaque échantillon par cluster, calculé comme la différence
entre la distance moyenne intra-cluster et la distance moyenne
au plus proche cluster pour chaque échantillon,
normalisée par la valeur maximale.
Cela donne un score entre -1 et +1, où les scores proches de +1 indiquent
une séparation élevée et les scores proches de -1 indiquent
que les échantillons peuvent avoir été affectés au mauvais cluster.
Dans les graphiques de SilhouetteVisualizer, les clusters ayant des scores
plus élevés ont des silhouettes plus larges, mais les grappes qui sont moins
cohésives n'atteignent pas le score moyen de toutes les cluster,
qui est représenté par une ligne verticale pointillée en rouge.
model = KMeans(3)
visualizer = SilhouetteVisualizer(model)
visualizer.fit(df5std) # Fit the data to the visualizer
visualizer.poof() # Draw/show/poof the data
<AxesSubplot:title={'center':'Silhouette Plot of KMeans Clustering for 93350 Samples in 3 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
Les clusters ne sont pas très homogènes.
Ils se distinguent tous par une population
et un score de coefficient de silhouette moyen nettement différents.
Un des clusters se démarque particulièrement des deux autres par une population
beaucoup plus faible et un coefficient de silhouette nettement plus élevé.
Enfin, aucun des clusters n'a d'échantillon avec un coefficient de silhouette inférieur à 0.
model = KMeans(9)
visualizer = SilhouetteVisualizer(model)
visualizer.fit(df5std) # Fit the data to the visualizer
visualizer.poof() # Draw/show/poof the data
<AxesSubplot:title={'center':'Silhouette Plot of KMeans Clustering for 93350 Samples in 9 Centers'}, xlabel='silhouette coefficient values', ylabel='cluster label'>
Ici aussi les clusters ne semblent pas très homogènes entre-eux.
De plus certains clusters ont des échantillons de leur population
avec des coefficients de silhouette inférieurs à 0 et atteignant parfois -0.8.
On retrouve cependant notre cluster ayant une faible population
mais un coefficient de silhouette élevé.
A ce stade, avec 5 features, il me semble plus pertinent
de partitionner nos clients en 3 clusters.
Utilisation de K-Means pour calculer 3 et 9 clusters,
les valeurs optimales en nombre de clusters d'après le coefficient de silhouette.
for i in [3,9]:
print('n_clusters =',i)
df5['cluster_number_for_'+str(i)+'_clusters'] =\
pd.DataFrame(data=KMeans(n_clusters=i).fit(df5std).labels_,
index=df5.index)
n_clusters = 3 n_clusters = 9
df5.head(3).T
| 0 | 1 | 2 | |
|---|---|---|---|
| recency | 470.00000 | 228.00000 | 101.00000 |
| frequency | 1.00000 | 1.00000 | 1.00000 |
| amount | 146.87000 | 335.48000 | 157.73000 |
| satisfaction | 4.00000 | 5.00000 | 5.00000 |
| delivery | 11.00000 | 8.00000 | -1.00000 |
| cluster_number_for_3_clusters | 2.00000 | 1.00000 | 1.00000 |
| cluster_number_for_9_clusters | 7.00000 | 2.00000 | 6.00000 |
J'applique une Analyse en Composante Principale à mon jeu de donnée
en le ramenant en 2 dimensions afin de pouvoir visualiser graphiquement
les différents clusters :
pca = PCA(n_components=2)
pca.fit(df5std)
print('Variance explained by the two principals components:',
pca.explained_variance_ratio_.cumsum()[-1])
X_pca = pca.transform(df5std)
Variance explained by the two principals components: 0.4912358500623355
Affichage des clusters en fonction du nombre total de clusters :
plt.figure(figsize=(20, 10))
nbColumns = 3
nbRows = len(df5.columns[5:])//nbColumns +\
(len(df5.columns[5:])%nbColumns > 0)
for i,j in enumerate(df5.columns[5:]):
# Extract the number of clusters
clusterNumber = int(re.sub('cluster_number_for_',
'',
re.sub('\_clusters$',
'',
j)))
# Plot Clusters
plt.subplot(nbRows,nbColumns,i+1)
plt.scatter(X_pca[:, 0],
X_pca[:, 1],
c=df5[j],
cmap="viridis")
plt.title(f'Number of cluster: {clusterNumber}')
print('-----------------------------')
print('Number of clusters:',clusterNumber)
for k,l in enumerate(df5[j].value_counts().sort_index()):
print(f'Cluster n°{k} -- number of customers: {l}')
print('-----------------------------')
----------------------------- Number of clusters: 3 Cluster n°0 -- number of customers: 2801 Cluster n°1 -- number of customers: 53095 Cluster n°2 -- number of customers: 37454 ----------------------------- ----------------------------- Number of clusters: 9 Cluster n°0 -- number of customers: 8212 Cluster n°1 -- number of customers: 9684 Cluster n°2 -- number of customers: 15802 Cluster n°3 -- number of customers: 2801 Cluster n°4 -- number of customers: 9051 Cluster n°5 -- number of customers: 14485 Cluster n°6 -- number of customers: 15157 Cluster n°7 -- number of customers: 11282 Cluster n°8 -- number of customers: 6876 -----------------------------
Affichage du nombre de Clients/Cluster en fonction du nombre de clusters total :
for i,j in enumerate(df5.iloc[:,5:]):
clusterNumber = int(re.sub('cluster_number_for_',
'',
re.sub('\_clusters$',
'',
j)))
plt.figure(figsize=(12,7))
plt.hist(df5[j],
range=(0, clusterNumber),
bins=clusterNumber,
color='yellow',
edgecolor = 'red',
align='left')
plt.title(j)
plt.xlabel('Cluster Number')
plt.ylabel('Number of Custumer')
J'affiche pour chaque feature,
les différents clusters sous forme de boxplot
en fonction du nombre de clusters total.
Cette visualisation doit nous permettre de comprendre
et d'interpréter les profils des différents clients.
for i,j in enumerate(df5.iloc[:,5:]):
clusterNumber = int(re.sub('cluster_number_for_',
'',
re.sub('\_clusters$',
'',
j)))
plt.figure(figsize=(20,10))
for k in range(5):
plt.subplot(1,5,k+1)
plt.title(f'Number of cluster: {clusterNumber} - Feature: {df5.columns[k]}')
sns.boxplot(x=df5.iloc[:,i+5],
y=df5.iloc[:,k],
showfliers = False,
showmeans=True)
Interpretation lorsque le jeu de donnée est divisé en :
Le diagramme de Kiviat nous permet de visualiser rapidement
et efficacement le profil client de chaque cluster.
Le graphique affiche les valeurs moyennes, normalisés entre 0 et 1
de chaque features, pour chaque cluster.
Cela permet de déterminer d'un seul coup le profil type
de chaque groupe de client que KMeans a identifié.
df5_kiviat = df5.copy()
df5_kiviat[['recency',
'frequency',
'amount',
'satisfaction',
'delivery']] = MinMaxScaler()\
.fit_transform(PowerTransformer()\
.fit_transform(df5_kiviat[['recency',
'frequency',
'amount',
'satisfaction',
'delivery']]))
Je ne conserve que le nombre de *clusters optimal*
pour l'affichage des **Diagrammes de Kiviat** :
df5_kiviat = df5_kiviat[['recency',
'frequency',
'amount',
'satisfaction',
'delivery',
'cluster_number_for_3_clusters']]
plt.figure(figsize=(20,10))
for k in range(5):
plt.subplot(1,5,k+1)
plt.title(f'Cluster number: 3 - Feature: {df5_kiviat.columns[k]}')
sns.boxplot(x=df5_kiviat.iloc[:,-1],
y=df5_kiviat.iloc[:,k],
showfliers = False,
showmeans=True)
nbColumns = 3
nbRows = df5_kiviat.iloc[:,-1].nunique()//nbColumns +\
(df5_kiviat.iloc[:,-1].nunique()%nbColumns > 0)
colClusters = df5_kiviat.columns[-1]
plt.figure(figsize=(15,5*nbRows))
for i in sorted(df5_kiviat.iloc[:,-1].unique()):
# number of variable
categories=list(df5_kiviat)[:-1]
N = len(categories)
# We are going to plot the first line of the data frame.
# But we need to repeat the first value to close the circular graph:
values=df5_kiviat[df5_kiviat[colClusters] == i]\
.drop(colClusters, axis=1)\
.mean().values.tolist()
values += values[:1]
# What will be the angle of each axis in the plot?
# (we divide the plot / number of variable)
angles = [n / float(N) * 2 * pi for n in range(N)]
angles += angles[:1]
# Initialise the spider plot
ax = plt.subplot(nbRows,nbColumns,i+1, polar=True)
# Draw one axe per variable + add labels labels yet
plt.xticks(angles[:-1],
categories,
color='grey',
size=8,
ha='left')
# Draw ylabels
ax.set_rlabel_position(0)
plt.yticks([0,0.5,1], ["0","0.5","1"],
color="grey",
size=7)
plt.ylim(0,1)
# Plot data
ax.plot(angles,
values,
linewidth=1,
linestyle='solid')
# Fill area
ax.fill(angles,
values,
'b',
alpha=0.1)
plt.title(f'Cluster n°{i}')
DBSCAN est un algorithme de clustering qui s’appuie sur la densité
estimée des clusters pour effectuer le partitionnement.
L'algorithme DBSCAN utilise 2 paramètres :
Les paramètres d'entrées sont donc une estimation de la densité
de points des clusters.
L'idée de base de l'algorithme est ensuite, pour un point donné,
de récupérer son epsilon-voisinage et de vérifier qu'il contient bien
MinPts points ou plus.
Ce point est alors considéré comme faisant partie d'un cluster.
On parcourt ensuite l'epsilon-voisinage de proche en proche afin
de trouver l'ensemble des points du cluster.
Contrairement à KMeans, ici nous ne choisissons pas
à l'avance le nombre de Cluster.
Suppression des colonnes contenant les clusters K-Means dans **df5std** et **df5** :
df5std = df5std.iloc[:,:5]
df5 = df5.iloc[:,:5]
Test de l'algorithme DBSCAN pour **EPS allant de 0.1 à 1.5** par pas de **0.1** :
coeffSilhouette = []
numberOfCluster = []
for i in range(1,16):
print('eps =',i/10)
db5=DBSCAN(eps=i/10, min_samples=10,).fit(df5std)
# Number of clusters in labels, ignoring noise if present.
n_clusters_ = len(set(db5.labels_)) - (1 if -1 in db5.labels_ else 0)
coeffSilhouette.append(silhouette_score(df5std, db5.labels_))
numberOfCluster.append(n_clusters_)
# I assign the calculated labels in a new column of the DataFrame df5
# for future analysis of the number of customers per cluster.
df5['cluster_for_EPS_'+str(i/10)] =\
pd.DataFrame(data=db5.labels_,
index=df5.index)
eps = 0.1 eps = 0.2 eps = 0.3 eps = 0.4 eps = 0.5 eps = 0.6 eps = 0.7 eps = 0.8 eps = 0.9 eps = 1.0 eps = 1.1 eps = 1.2 eps = 1.3 eps = 1.4 eps = 1.5
# Sauvegarde des List coeffSilhouette et numberOfCluster
pickle.dump(coeffSilhouette, open('dbscan_coeffSilhouette_5f', 'wb'))
pickle.dump(numberOfCluster, open('dbscan_numberOfCluster_5f', 'wb'))
# Restauration des List coeffSilhouette et numberOfCluster
# coeffSilhouette = pickle.load(open('dbscan_coeffSilhouette_5f', 'rb'))
# numberOfCluster = pickle.load(open('dbscan_numberOfCluster_5f', 'rb'))
Pour chaque **EPS** testé, affichage du nombre de clusters et du coefficient de silhouette :
plt.figure(figsize=(12,12))
plt.subplot(211)
plt.plot(list(map(str, np.linspace(1,15,15,dtype='int')/10)), coeffSilhouette)
plt.grid()
plt.xlabel('EPS')
plt.ylabel('Silhouette Coefficient')
plt.title('Silhouette coefficient according to EPS value')
plt.subplot(212)
plt.bar(list(map(str, np.linspace(1,15,15,dtype='int')/10)), numberOfCluster)
plt.grid()
plt.xlabel('EPS')
plt.ylabel('Number of Clusters')
plt.title('Number of Clusters according to EPS value')
Text(0.5, 1.0, 'Number of Clusters according to EPS value')
Affichage du nombre de clients / cluster en fonction de **EPS** :
for i,j in enumerate(df5.columns[5:]):
print('-----------------------------')
print('Number of Clusters for EPS =',
(i+1)/10,
':',
df5[j].nunique())
for index, value in df5[j].value_counts().sort_index().items():
print(f'Cluster n°{index} -- Number of customers: {value}')
print('-----------------------------')
----------------------------- Number of Clusters for EPS = 0.1 : 539 Cluster n°-1 -- Number of customers: 66278 Cluster n°0 -- Number of customers: 2079 Cluster n°1 -- Number of customers: 1025 Cluster n°2 -- Number of customers: 2725 Cluster n°3 -- Number of customers: 544 Cluster n°4 -- Number of customers: 481 Cluster n°5 -- Number of customers: 972 Cluster n°6 -- Number of customers: 122 Cluster n°7 -- Number of customers: 14 Cluster n°8 -- Number of customers: 518 Cluster n°9 -- Number of customers: 1008 Cluster n°10 -- Number of customers: 158 Cluster n°11 -- Number of customers: 1155 Cluster n°12 -- Number of customers: 88 Cluster n°13 -- Number of customers: 27 Cluster n°14 -- Number of customers: 316 Cluster n°15 -- Number of customers: 1733 Cluster n°16 -- Number of customers: 198 Cluster n°17 -- Number of customers: 81 Cluster n°18 -- Number of customers: 437 Cluster n°19 -- Number of customers: 63 Cluster n°20 -- Number of customers: 488 Cluster n°21 -- Number of customers: 241 Cluster n°22 -- Number of customers: 184 Cluster n°23 -- Number of customers: 55 Cluster n°24 -- Number of customers: 21 Cluster n°25 -- Number of customers: 46 Cluster n°26 -- Number of customers: 15 Cluster n°27 -- Number of customers: 17 Cluster n°28 -- Number of customers: 55 Cluster n°29 -- Number of customers: 10 Cluster n°30 -- Number of customers: 95 Cluster n°31 -- Number of customers: 22 Cluster n°32 -- Number of customers: 40 Cluster n°33 -- Number of customers: 134 Cluster n°34 -- Number of customers: 16 Cluster n°35 -- Number of customers: 10 Cluster n°36 -- Number of customers: 17 Cluster n°37 -- Number of customers: 17 Cluster n°38 -- Number of customers: 75 Cluster n°39 -- Number of customers: 11 Cluster n°40 -- Number of customers: 70 Cluster n°41 -- Number of customers: 78 Cluster n°42 -- Number of customers: 32 Cluster n°43 -- Number of customers: 47 Cluster n°44 -- Number of customers: 38 Cluster n°45 -- Number of customers: 30 Cluster n°46 -- Number of customers: 39 Cluster n°47 -- Number of customers: 91 Cluster n°48 -- Number of customers: 66 Cluster n°49 -- Number of customers: 29 Cluster n°50 -- Number of customers: 122 Cluster n°51 -- Number of customers: 12 Cluster n°52 -- Number of customers: 251 Cluster n°53 -- Number of customers: 15 Cluster n°54 -- Number of customers: 213 Cluster n°55 -- Number of customers: 14 Cluster n°56 -- Number of customers: 32 Cluster n°57 -- Number of customers: 30 Cluster n°58 -- Number of customers: 52 Cluster n°59 -- Number of customers: 121 Cluster n°60 -- Number of customers: 10 Cluster n°61 -- Number of customers: 158 Cluster n°62 -- Number of customers: 56 Cluster n°63 -- Number of customers: 446 Cluster n°64 -- Number of customers: 14 Cluster n°65 -- Number of customers: 11 Cluster n°66 -- Number of customers: 139 Cluster n°67 -- Number of customers: 56 Cluster n°68 -- Number of customers: 153 Cluster n°69 -- Number of customers: 17 Cluster n°70 -- Number of customers: 11 Cluster n°71 -- Number of customers: 49 Cluster n°72 -- Number of customers: 28 Cluster n°73 -- Number of customers: 84 Cluster n°74 -- Number of customers: 31 Cluster n°75 -- Number of customers: 21 Cluster n°76 -- Number of customers: 14 Cluster n°77 -- Number of customers: 22 Cluster n°78 -- Number of customers: 14 Cluster n°79 -- Number of customers: 22 Cluster n°80 -- Number of customers: 17 Cluster n°81 -- Number of customers: 10 Cluster n°82 -- Number of customers: 45 Cluster n°83 -- Number of customers: 41 Cluster n°84 -- Number of customers: 10 Cluster n°85 -- Number of customers: 20 Cluster n°86 -- Number of customers: 54 Cluster n°87 -- Number of customers: 23 Cluster n°88 -- Number of customers: 26 Cluster n°89 -- Number of customers: 56 Cluster n°90 -- Number of customers: 46 Cluster n°91 -- Number of customers: 22 Cluster n°92 -- Number of customers: 47 Cluster n°93 -- Number of customers: 40 Cluster n°94 -- Number of customers: 32 Cluster n°95 -- Number of customers: 37 Cluster n°96 -- Number of customers: 30 Cluster n°97 -- Number of customers: 107 Cluster n°98 -- Number of customers: 40 Cluster n°99 -- Number of customers: 22 Cluster n°100 -- Number of customers: 24 Cluster n°101 -- Number of customers: 240 Cluster n°102 -- Number of customers: 19 Cluster n°103 -- Number of customers: 92 Cluster n°104 -- Number of customers: 74 Cluster n°105 -- Number of customers: 25 Cluster n°106 -- Number of customers: 72 Cluster n°107 -- Number of customers: 67 Cluster n°108 -- Number of customers: 48 Cluster n°109 -- Number of customers: 23 Cluster n°110 -- Number of customers: 29 Cluster n°111 -- Number of customers: 64 Cluster n°112 -- Number of customers: 10 Cluster n°113 -- Number of customers: 46 Cluster n°114 -- Number of customers: 20 Cluster n°115 -- Number of customers: 11 Cluster n°116 -- Number of customers: 143 Cluster n°117 -- Number of customers: 37 Cluster n°118 -- Number of customers: 14 Cluster n°119 -- Number of customers: 14 Cluster n°120 -- Number of customers: 40 Cluster n°121 -- Number of customers: 17 Cluster n°122 -- Number of customers: 93 Cluster n°123 -- Number of customers: 25 Cluster n°124 -- Number of customers: 13 Cluster n°125 -- Number of customers: 48 Cluster n°126 -- Number of customers: 58 Cluster n°127 -- Number of customers: 15 Cluster n°128 -- Number of customers: 10 Cluster n°129 -- Number of customers: 12 Cluster n°130 -- Number of customers: 33 Cluster n°131 -- Number of customers: 50 Cluster n°132 -- Number of customers: 50 Cluster n°133 -- Number of customers: 71 Cluster n°134 -- Number of customers: 27 Cluster n°135 -- Number of customers: 21 Cluster n°136 -- Number of customers: 46 Cluster n°137 -- Number of customers: 35 Cluster n°138 -- Number of customers: 29 Cluster n°139 -- Number of customers: 58 Cluster n°140 -- Number of customers: 23 Cluster n°141 -- Number of customers: 51 Cluster n°142 -- Number of customers: 31 Cluster n°143 -- Number of customers: 19 Cluster n°144 -- Number of customers: 17 Cluster n°145 -- Number of customers: 24 Cluster n°146 -- Number of customers: 64 Cluster n°147 -- Number of customers: 27 Cluster n°148 -- Number of customers: 23 Cluster n°149 -- Number of customers: 18 Cluster n°150 -- Number of customers: 46 Cluster n°151 -- Number of customers: 10 Cluster n°152 -- Number of customers: 18 Cluster n°153 -- Number of customers: 15 Cluster n°154 -- Number of customers: 20 Cluster n°155 -- Number of customers: 53 Cluster n°156 -- Number of customers: 13 Cluster n°157 -- Number of customers: 37 Cluster n°158 -- Number of customers: 15 Cluster n°159 -- Number of customers: 11 Cluster n°160 -- Number of customers: 22 Cluster n°161 -- Number of customers: 15 Cluster n°162 -- Number of customers: 10 Cluster n°163 -- Number of customers: 26 Cluster n°164 -- Number of customers: 42 Cluster n°165 -- Number of customers: 42 Cluster n°166 -- Number of customers: 34 Cluster n°167 -- Number of customers: 93 Cluster n°168 -- Number of customers: 19 Cluster n°169 -- Number of customers: 57 Cluster n°170 -- Number of customers: 20 Cluster n°171 -- Number of customers: 45 Cluster n°172 -- Number of customers: 20 Cluster n°173 -- Number of customers: 24 Cluster n°174 -- Number of customers: 6 Cluster n°175 -- Number of customers: 28 Cluster n°176 -- Number of customers: 16 Cluster n°177 -- Number of customers: 16 Cluster n°178 -- Number of customers: 50 Cluster n°179 -- Number of customers: 14 Cluster n°180 -- Number of customers: 15 Cluster n°181 -- Number of customers: 10 Cluster n°182 -- Number of customers: 24 Cluster n°183 -- Number of customers: 10 Cluster n°184 -- Number of customers: 21 Cluster n°185 -- Number of customers: 12 Cluster n°186 -- Number of customers: 30 Cluster n°187 -- Number of customers: 10 Cluster n°188 -- Number of customers: 39 Cluster n°189 -- Number of customers: 12 Cluster n°190 -- Number of customers: 18 Cluster n°191 -- Number of customers: 10 Cluster n°192 -- Number of customers: 18 Cluster n°193 -- Number of customers: 21 Cluster n°194 -- Number of customers: 15 Cluster n°195 -- Number of customers: 13 Cluster n°196 -- Number of customers: 17 Cluster n°197 -- Number of customers: 17 Cluster n°198 -- Number of customers: 84 Cluster n°199 -- Number of customers: 11 Cluster n°200 -- Number of customers: 17 Cluster n°201 -- Number of customers: 14 Cluster n°202 -- Number of customers: 54 Cluster n°203 -- Number of customers: 12 Cluster n°204 -- Number of customers: 13 Cluster n°205 -- Number of customers: 15 Cluster n°206 -- Number of customers: 26 Cluster n°207 -- Number of customers: 16 Cluster n°208 -- Number of customers: 22 Cluster n°209 -- Number of customers: 28 Cluster n°210 -- Number of customers: 33 Cluster n°211 -- Number of customers: 14 Cluster n°212 -- Number of customers: 12 Cluster n°213 -- Number of customers: 16 Cluster n°214 -- Number of customers: 12 Cluster n°215 -- Number of customers: 13 Cluster n°216 -- Number of customers: 10 Cluster n°217 -- Number of customers: 15 Cluster n°218 -- Number of customers: 17 Cluster n°219 -- Number of customers: 34 Cluster n°220 -- Number of customers: 18 Cluster n°221 -- Number of customers: 23 Cluster n°222 -- Number of customers: 16 Cluster n°223 -- Number of customers: 16 Cluster n°224 -- Number of customers: 14 Cluster n°225 -- Number of customers: 20 Cluster n°226 -- Number of customers: 12 Cluster n°227 -- Number of customers: 26 Cluster n°228 -- Number of customers: 17 Cluster n°229 -- Number of customers: 23 Cluster n°230 -- Number of customers: 12 Cluster n°231 -- Number of customers: 10 Cluster n°232 -- Number of customers: 16 Cluster n°233 -- Number of customers: 12 Cluster n°234 -- Number of customers: 10 Cluster n°235 -- Number of customers: 18 Cluster n°236 -- Number of customers: 10 Cluster n°237 -- Number of customers: 32 Cluster n°238 -- Number of customers: 31 Cluster n°239 -- Number of customers: 21 Cluster n°240 -- Number of customers: 14 Cluster n°241 -- Number of customers: 21 Cluster n°242 -- Number of customers: 10 Cluster n°243 -- Number of customers: 28 Cluster n°244 -- Number of customers: 19 Cluster n°245 -- Number of customers: 17 Cluster n°246 -- Number of customers: 13 Cluster n°247 -- Number of customers: 22 Cluster n°248 -- Number of customers: 10 Cluster n°249 -- Number of customers: 12 Cluster n°250 -- Number of customers: 38 Cluster n°251 -- Number of customers: 17 Cluster n°252 -- Number of customers: 15 Cluster n°253 -- Number of customers: 9 Cluster n°254 -- Number of customers: 14 Cluster n°255 -- Number of customers: 33 Cluster n°256 -- Number of customers: 19 Cluster n°257 -- Number of customers: 21 Cluster n°258 -- Number of customers: 30 Cluster n°259 -- Number of customers: 28 Cluster n°260 -- Number of customers: 23 Cluster n°261 -- Number of customers: 10 Cluster n°262 -- Number of customers: 18 Cluster n°263 -- Number of customers: 27 Cluster n°264 -- Number of customers: 23 Cluster n°265 -- Number of customers: 17 Cluster n°266 -- Number of customers: 12 Cluster n°267 -- Number of customers: 30 Cluster n°268 -- Number of customers: 10 Cluster n°269 -- Number of customers: 22 Cluster n°270 -- Number of customers: 14 Cluster n°271 -- Number of customers: 31 Cluster n°272 -- Number of customers: 17 Cluster n°273 -- Number of customers: 10 Cluster n°274 -- Number of customers: 10 Cluster n°275 -- Number of customers: 14 Cluster n°276 -- Number of customers: 13 Cluster n°277 -- Number of customers: 26 Cluster n°278 -- Number of customers: 5 Cluster n°279 -- Number of customers: 13 Cluster n°280 -- Number of customers: 41 Cluster n°281 -- Number of customers: 32 Cluster n°282 -- Number of customers: 19 Cluster n°283 -- Number of customers: 14 Cluster n°284 -- Number of customers: 13 Cluster n°285 -- Number of customers: 19 Cluster n°286 -- Number of customers: 18 Cluster n°287 -- Number of customers: 14 Cluster n°288 -- Number of customers: 18 Cluster n°289 -- Number of customers: 15 Cluster n°290 -- Number of customers: 10 Cluster n°291 -- Number of customers: 25 Cluster n°292 -- Number of customers: 16 Cluster n°293 -- Number of customers: 21 Cluster n°294 -- Number of customers: 24 Cluster n°295 -- Number of customers: 8 Cluster n°296 -- Number of customers: 12 Cluster n°297 -- Number of customers: 21 Cluster n°298 -- Number of customers: 19 Cluster n°299 -- Number of customers: 17 Cluster n°300 -- Number of customers: 16 Cluster n°301 -- Number of customers: 17 Cluster n°302 -- Number of customers: 16 Cluster n°303 -- Number of customers: 7 Cluster n°304 -- Number of customers: 11 Cluster n°305 -- Number of customers: 10 Cluster n°306 -- Number of customers: 14 Cluster n°307 -- Number of customers: 10 Cluster n°308 -- Number of customers: 14 Cluster n°309 -- Number of customers: 7 Cluster n°310 -- Number of customers: 18 Cluster n°311 -- Number of customers: 16 Cluster n°312 -- Number of customers: 10 Cluster n°313 -- Number of customers: 19 Cluster n°314 -- Number of customers: 46 Cluster n°315 -- Number of customers: 15 Cluster n°316 -- Number of customers: 13 Cluster n°317 -- Number of customers: 15 Cluster n°318 -- Number of customers: 25 Cluster n°319 -- Number of customers: 10 Cluster n°320 -- Number of customers: 18 Cluster n°321 -- Number of customers: 15 Cluster n°322 -- Number of customers: 18 Cluster n°323 -- Number of customers: 33 Cluster n°324 -- Number of customers: 10 Cluster n°325 -- Number of customers: 16 Cluster n°326 -- Number of customers: 28 Cluster n°327 -- Number of customers: 11 Cluster n°328 -- Number of customers: 15 Cluster n°329 -- Number of customers: 38 Cluster n°330 -- Number of customers: 19 Cluster n°331 -- Number of customers: 16 Cluster n°332 -- Number of customers: 10 Cluster n°333 -- Number of customers: 17 Cluster n°334 -- Number of customers: 18 Cluster n°335 -- Number of customers: 14 Cluster n°336 -- Number of customers: 15 Cluster n°337 -- Number of customers: 22 Cluster n°338 -- Number of customers: 10 Cluster n°339 -- Number of customers: 12 Cluster n°340 -- Number of customers: 16 Cluster n°341 -- Number of customers: 10 Cluster n°342 -- Number of customers: 10 Cluster n°343 -- Number of customers: 10 Cluster n°344 -- Number of customers: 9 Cluster n°345 -- Number of customers: 10 Cluster n°346 -- Number of customers: 17 Cluster n°347 -- Number of customers: 13 Cluster n°348 -- Number of customers: 19 Cluster n°349 -- Number of customers: 16 Cluster n°350 -- Number of customers: 10 Cluster n°351 -- Number of customers: 18 Cluster n°352 -- Number of customers: 13 Cluster n°353 -- Number of customers: 19 Cluster n°354 -- Number of customers: 10 Cluster n°355 -- Number of customers: 24 Cluster n°356 -- Number of customers: 32 Cluster n°357 -- Number of customers: 10 Cluster n°358 -- Number of customers: 16 Cluster n°359 -- Number of customers: 18 Cluster n°360 -- Number of customers: 10 Cluster n°361 -- Number of customers: 10 Cluster n°362 -- Number of customers: 12 Cluster n°363 -- Number of customers: 17 Cluster n°364 -- Number of customers: 22 Cluster n°365 -- Number of customers: 15 Cluster n°366 -- Number of customers: 15 Cluster n°367 -- Number of customers: 11 Cluster n°368 -- Number of customers: 10 Cluster n°369 -- Number of customers: 10 Cluster n°370 -- Number of customers: 20 Cluster n°371 -- Number of customers: 12 Cluster n°372 -- Number of customers: 10 Cluster n°373 -- Number of customers: 21 Cluster n°374 -- Number of customers: 15 Cluster n°375 -- Number of customers: 10 Cluster n°376 -- Number of customers: 23 Cluster n°377 -- Number of customers: 10 Cluster n°378 -- Number of customers: 8 Cluster n°379 -- Number of customers: 14 Cluster n°380 -- Number of customers: 10 Cluster n°381 -- Number of customers: 11 Cluster n°382 -- Number of customers: 10 Cluster n°383 -- Number of customers: 18 Cluster n°384 -- Number of customers: 13 Cluster n°385 -- Number of customers: 20 Cluster n°386 -- Number of customers: 11 Cluster n°387 -- Number of customers: 10 Cluster n°388 -- Number of customers: 10 Cluster n°389 -- Number of customers: 23 Cluster n°390 -- Number of customers: 10 Cluster n°391 -- Number of customers: 15 Cluster n°392 -- Number of customers: 12 Cluster n°393 -- Number of customers: 10 Cluster n°394 -- Number of customers: 13 Cluster n°395 -- Number of customers: 9 Cluster n°396 -- Number of customers: 23 Cluster n°397 -- Number of customers: 13 Cluster n°398 -- Number of customers: 13 Cluster n°399 -- Number of customers: 13 Cluster n°400 -- Number of customers: 15 Cluster n°401 -- Number of customers: 18 Cluster n°402 -- Number of customers: 16 Cluster n°403 -- Number of customers: 13 Cluster n°404 -- Number of customers: 10 Cluster n°405 -- Number of customers: 27 Cluster n°406 -- Number of customers: 13 Cluster n°407 -- Number of customers: 10 Cluster n°408 -- Number of customers: 12 Cluster n°409 -- Number of customers: 7 Cluster n°410 -- Number of customers: 11 Cluster n°411 -- Number of customers: 11 Cluster n°412 -- Number of customers: 7 Cluster n°413 -- Number of customers: 10 Cluster n°414 -- Number of customers: 13 Cluster n°415 -- Number of customers: 13 Cluster n°416 -- Number of customers: 12 Cluster n°417 -- Number of customers: 12 Cluster n°418 -- Number of customers: 12 Cluster n°419 -- Number of customers: 10 Cluster n°420 -- Number of customers: 12 Cluster n°421 -- Number of customers: 10 Cluster n°422 -- Number of customers: 16 Cluster n°423 -- Number of customers: 26 Cluster n°424 -- Number of customers: 17 Cluster n°425 -- Number of customers: 10 Cluster n°426 -- Number of customers: 33 Cluster n°427 -- Number of customers: 10 Cluster n°428 -- Number of customers: 10 Cluster n°429 -- Number of customers: 10 Cluster n°430 -- Number of customers: 10 Cluster n°431 -- Number of customers: 11 Cluster n°432 -- Number of customers: 9 Cluster n°433 -- Number of customers: 9 Cluster n°434 -- Number of customers: 14 Cluster n°435 -- Number of customers: 10 Cluster n°436 -- Number of customers: 13 Cluster n°437 -- Number of customers: 10 Cluster n°438 -- Number of customers: 12 Cluster n°439 -- Number of customers: 6 Cluster n°440 -- Number of customers: 10 Cluster n°441 -- Number of customers: 12 Cluster n°442 -- Number of customers: 11 Cluster n°443 -- Number of customers: 10 Cluster n°444 -- Number of customers: 16 Cluster n°445 -- Number of customers: 11 Cluster n°446 -- Number of customers: 17 Cluster n°447 -- Number of customers: 10 Cluster n°448 -- Number of customers: 7 Cluster n°449 -- Number of customers: 10 Cluster n°450 -- Number of customers: 10 Cluster n°451 -- Number of customers: 9 Cluster n°452 -- Number of customers: 14 Cluster n°453 -- Number of customers: 16 Cluster n°454 -- Number of customers: 11 Cluster n°455 -- Number of customers: 13 Cluster n°456 -- Number of customers: 10 Cluster n°457 -- Number of customers: 10 Cluster n°458 -- Number of customers: 13 Cluster n°459 -- Number of customers: 14 Cluster n°460 -- Number of customers: 10 Cluster n°461 -- Number of customers: 12 Cluster n°462 -- Number of customers: 14 Cluster n°463 -- Number of customers: 10 Cluster n°464 -- Number of customers: 11 Cluster n°465 -- Number of customers: 14 Cluster n°466 -- Number of customers: 14 Cluster n°467 -- Number of customers: 11 Cluster n°468 -- Number of customers: 10 Cluster n°469 -- Number of customers: 10 Cluster n°470 -- Number of customers: 13 Cluster n°471 -- Number of customers: 10 Cluster n°472 -- Number of customers: 10 Cluster n°473 -- Number of customers: 13 Cluster n°474 -- Number of customers: 10 Cluster n°475 -- Number of customers: 14 Cluster n°476 -- Number of customers: 8 Cluster n°477 -- Number of customers: 13 Cluster n°478 -- Number of customers: 12 Cluster n°479 -- Number of customers: 10 Cluster n°480 -- Number of customers: 9 Cluster n°481 -- Number of customers: 11 Cluster n°482 -- Number of customers: 9 Cluster n°483 -- Number of customers: 15 Cluster n°484 -- Number of customers: 10 Cluster n°485 -- Number of customers: 8 Cluster n°486 -- Number of customers: 12 Cluster n°487 -- Number of customers: 10 Cluster n°488 -- Number of customers: 10 Cluster n°489 -- Number of customers: 10 Cluster n°490 -- Number of customers: 7 Cluster n°491 -- Number of customers: 10 Cluster n°492 -- Number of customers: 15 Cluster n°493 -- Number of customers: 10 Cluster n°494 -- Number of customers: 10 Cluster n°495 -- Number of customers: 10 Cluster n°496 -- Number of customers: 14 Cluster n°497 -- Number of customers: 6 Cluster n°498 -- Number of customers: 11 Cluster n°499 -- Number of customers: 10 Cluster n°500 -- Number of customers: 21 Cluster n°501 -- Number of customers: 10 Cluster n°502 -- Number of customers: 16 Cluster n°503 -- Number of customers: 11 Cluster n°504 -- Number of customers: 10 Cluster n°505 -- Number of customers: 10 Cluster n°506 -- Number of customers: 2 Cluster n°507 -- Number of customers: 13 Cluster n°508 -- Number of customers: 7 Cluster n°509 -- Number of customers: 10 Cluster n°510 -- Number of customers: 10 Cluster n°511 -- Number of customers: 12 Cluster n°512 -- Number of customers: 10 Cluster n°513 -- Number of customers: 10 Cluster n°514 -- Number of customers: 12 Cluster n°515 -- Number of customers: 10 Cluster n°516 -- Number of customers: 9 Cluster n°517 -- Number of customers: 10 Cluster n°518 -- Number of customers: 10 Cluster n°519 -- Number of customers: 8 Cluster n°520 -- Number of customers: 10 Cluster n°521 -- Number of customers: 10 Cluster n°522 -- Number of customers: 10 Cluster n°523 -- Number of customers: 10 Cluster n°524 -- Number of customers: 6 Cluster n°525 -- Number of customers: 10 Cluster n°526 -- Number of customers: 14 Cluster n°527 -- Number of customers: 10 Cluster n°528 -- Number of customers: 10 Cluster n°529 -- Number of customers: 10 Cluster n°530 -- Number of customers: 10 Cluster n°531 -- Number of customers: 10 Cluster n°532 -- Number of customers: 10 Cluster n°533 -- Number of customers: 10 Cluster n°534 -- Number of customers: 11 Cluster n°535 -- Number of customers: 8 Cluster n°536 -- Number of customers: 8 Cluster n°537 -- Number of customers: 7 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.2 : 110 Cluster n°-1 -- Number of customers: 16317 Cluster n°0 -- Number of customers: 14452 Cluster n°1 -- Number of customers: 50024 Cluster n°2 -- Number of customers: 10404 Cluster n°3 -- Number of customers: 16 Cluster n°4 -- Number of customers: 92 Cluster n°5 -- Number of customers: 492 Cluster n°6 -- Number of customers: 10 Cluster n°7 -- Number of customers: 72 Cluster n°8 -- Number of customers: 17 Cluster n°9 -- Number of customers: 10 Cluster n°10 -- Number of customers: 59 Cluster n°11 -- Number of customers: 29 Cluster n°12 -- Number of customers: 9 Cluster n°13 -- Number of customers: 40 Cluster n°14 -- Number of customers: 11 Cluster n°15 -- Number of customers: 25 Cluster n°16 -- Number of customers: 47 Cluster n°17 -- Number of customers: 22 Cluster n°18 -- Number of customers: 11 Cluster n°19 -- Number of customers: 42 Cluster n°20 -- Number of customers: 29 Cluster n°21 -- Number of customers: 10 Cluster n°22 -- Number of customers: 9 Cluster n°23 -- Number of customers: 16 Cluster n°24 -- Number of customers: 4 Cluster n°25 -- Number of customers: 28 Cluster n°26 -- Number of customers: 36 Cluster n°27 -- Number of customers: 7 Cluster n°28 -- Number of customers: 36 Cluster n°29 -- Number of customers: 28 Cluster n°30 -- Number of customers: 15 Cluster n°31 -- Number of customers: 7 Cluster n°32 -- Number of customers: 15 Cluster n°33 -- Number of customers: 17 Cluster n°34 -- Number of customers: 15 Cluster n°35 -- Number of customers: 11 Cluster n°36 -- Number of customers: 13 Cluster n°37 -- Number of customers: 24 Cluster n°38 -- Number of customers: 7 Cluster n°39 -- Number of customers: 39 Cluster n°40 -- Number of customers: 8 Cluster n°41 -- Number of customers: 10 Cluster n°42 -- Number of customers: 27 Cluster n°43 -- Number of customers: 15 Cluster n°44 -- Number of customers: 11 Cluster n°45 -- Number of customers: 14 Cluster n°46 -- Number of customers: 19 Cluster n°47 -- Number of customers: 10 Cluster n°48 -- Number of customers: 14 Cluster n°49 -- Number of customers: 8 Cluster n°50 -- Number of customers: 11 Cluster n°51 -- Number of customers: 17 Cluster n°52 -- Number of customers: 15 Cluster n°53 -- Number of customers: 11 Cluster n°54 -- Number of customers: 8 Cluster n°55 -- Number of customers: 13 Cluster n°56 -- Number of customers: 10 Cluster n°57 -- Number of customers: 19 Cluster n°58 -- Number of customers: 21 Cluster n°59 -- Number of customers: 10 Cluster n°60 -- Number of customers: 11 Cluster n°61 -- Number of customers: 8 Cluster n°62 -- Number of customers: 8 Cluster n°63 -- Number of customers: 13 Cluster n°64 -- Number of customers: 10 Cluster n°65 -- Number of customers: 11 Cluster n°66 -- Number of customers: 14 Cluster n°67 -- Number of customers: 14 Cluster n°68 -- Number of customers: 9 Cluster n°69 -- Number of customers: 12 Cluster n°70 -- Number of customers: 8 Cluster n°71 -- Number of customers: 14 Cluster n°72 -- Number of customers: 9 Cluster n°73 -- Number of customers: 15 Cluster n°74 -- Number of customers: 10 Cluster n°75 -- Number of customers: 7 Cluster n°76 -- Number of customers: 15 Cluster n°77 -- Number of customers: 7 Cluster n°78 -- Number of customers: 10 Cluster n°79 -- Number of customers: 10 Cluster n°80 -- Number of customers: 11 Cluster n°81 -- Number of customers: 12 Cluster n°82 -- Number of customers: 11 Cluster n°83 -- Number of customers: 7 Cluster n°84 -- Number of customers: 12 Cluster n°85 -- Number of customers: 17 Cluster n°86 -- Number of customers: 9 Cluster n°87 -- Number of customers: 10 Cluster n°88 -- Number of customers: 12 Cluster n°89 -- Number of customers: 15 Cluster n°90 -- Number of customers: 8 Cluster n°91 -- Number of customers: 8 Cluster n°92 -- Number of customers: 7 Cluster n°93 -- Number of customers: 10 Cluster n°94 -- Number of customers: 16 Cluster n°95 -- Number of customers: 9 Cluster n°96 -- Number of customers: 11 Cluster n°97 -- Number of customers: 10 Cluster n°98 -- Number of customers: 11 Cluster n°99 -- Number of customers: 10 Cluster n°100 -- Number of customers: 14 Cluster n°101 -- Number of customers: 10 Cluster n°102 -- Number of customers: 10 Cluster n°103 -- Number of customers: 10 Cluster n°104 -- Number of customers: 9 Cluster n°105 -- Number of customers: 8 Cluster n°106 -- Number of customers: 7 Cluster n°107 -- Number of customers: 5 Cluster n°108 -- Number of customers: 8 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.3 : 17 Cluster n°-1 -- Number of customers: 4859 Cluster n°0 -- Number of customers: 35099 Cluster n°1 -- Number of customers: 52209 Cluster n°2 -- Number of customers: 20 Cluster n°3 -- Number of customers: 927 Cluster n°4 -- Number of customers: 16 Cluster n°5 -- Number of customers: 10 Cluster n°6 -- Number of customers: 69 Cluster n°7 -- Number of customers: 12 Cluster n°8 -- Number of customers: 15 Cluster n°9 -- Number of customers: 11 Cluster n°10 -- Number of customers: 11 Cluster n°11 -- Number of customers: 48 Cluster n°12 -- Number of customers: 7 Cluster n°13 -- Number of customers: 13 Cluster n°14 -- Number of customers: 14 Cluster n°15 -- Number of customers: 10 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.4 : 11 Cluster n°-1 -- Number of customers: 2133 Cluster n°0 -- Number of customers: 36520 Cluster n°1 -- Number of customers: 52617 Cluster n°2 -- Number of customers: 31 Cluster n°3 -- Number of customers: 824 Cluster n°4 -- Number of customers: 1172 Cluster n°5 -- Number of customers: 4 Cluster n°6 -- Number of customers: 6 Cluster n°7 -- Number of customers: 14 Cluster n°8 -- Number of customers: 19 Cluster n°9 -- Number of customers: 10 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.5 : 6 Cluster n°-1 -- Number of customers: 971 Cluster n°0 -- Number of customers: 36991 Cluster n°1 -- Number of customers: 52882 Cluster n°2 -- Number of customers: 1226 Cluster n°3 -- Number of customers: 1275 Cluster n°4 -- Number of customers: 5 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.6 : 5 Cluster n°-1 -- Number of customers: 479 Cluster n°0 -- Number of customers: 37232 Cluster n°1 -- Number of customers: 52961 Cluster n°2 -- Number of customers: 1365 Cluster n°3 -- Number of customers: 1313 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.7 : 4 Cluster n°-1 -- Number of customers: 282 Cluster n°0 -- Number of customers: 37335 Cluster n°1 -- Number of customers: 52998 Cluster n°2 -- Number of customers: 2735 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.8 : 4 Cluster n°-1 -- Number of customers: 183 Cluster n°0 -- Number of customers: 37382 Cluster n°1 -- Number of customers: 53016 Cluster n°2 -- Number of customers: 2769 ----------------------------- ----------------------------- Number of Clusters for EPS = 0.9 : 4 Cluster n°-1 -- Number of customers: 137 Cluster n°0 -- Number of customers: 37401 Cluster n°1 -- Number of customers: 53032 Cluster n°2 -- Number of customers: 2780 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.0 : 5 Cluster n°-1 -- Number of customers: 100 Cluster n°0 -- Number of customers: 37408 Cluster n°1 -- Number of customers: 53044 Cluster n°2 -- Number of customers: 2788 Cluster n°3 -- Number of customers: 10 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.1 : 5 Cluster n°-1 -- Number of customers: 80 Cluster n°0 -- Number of customers: 37413 Cluster n°1 -- Number of customers: 53055 Cluster n°2 -- Number of customers: 2791 Cluster n°3 -- Number of customers: 11 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.2 : 4 Cluster n°-1 -- Number of customers: 72 Cluster n°0 -- Number of customers: 37416 Cluster n°1 -- Number of customers: 53068 Cluster n°2 -- Number of customers: 2794 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.3 : 4 Cluster n°-1 -- Number of customers: 54 Cluster n°0 -- Number of customers: 90490 Cluster n°1 -- Number of customers: 2795 Cluster n°2 -- Number of customers: 11 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.4 : 4 Cluster n°-1 -- Number of customers: 35 Cluster n°0 -- Number of customers: 90500 Cluster n°1 -- Number of customers: 2796 Cluster n°2 -- Number of customers: 19 ----------------------------- ----------------------------- Number of Clusters for EPS = 1.5 : 3 Cluster n°-1 -- Number of customers: 29 Cluster n°0 -- Number of customers: 90524 Cluster n°1 -- Number of customers: 2797 -----------------------------
L'utilisation de DBSCAN dans notre cas est inexploitable
pour une utilisation métier car les clusters ne sont pas équilibrés.
Nous avons par exemple des clusters contenants une dizaine de clients
alors qu'un autre cluster en contient 92290.
Le dendrogramme est une arborescence qui affiche les groupes formés
par le regroupement des observations à chaque étape et leurs niveaux de similarité.
Le niveau de similarité est mesuré le long de l'axe vertical
et les différentes observations sont répertoriées le long de l'axe horizontal.
# setting distance_threshold=0 ensures we compute the full tree.
model = AgglomerativeClustering(distance_threshold=0, n_clusters=None)
# I select my data
X = df5std.iloc[:10000,:]
model = model.fit(X)
plt.figure(figsize=(10,10))
plt.title('Hierarchical Clustering Dendrogram')
# plot the top three levels of the dendrogram
plot_dendrogram(model, truncate_mode='level', p=3)
plt.xlabel("Number of points in node (or index of point if no parenthesis).")
plt.xticks(rotation=45)
plt.show()
Plusieurs remarques sont à prendre en compte sur l'utilisation de cette méthode :
Nous allons dans cette partie étudier
la stabilité des clusters au cours du temps.
A la suite à cette étude, nous réaliserons
une proposition de contrat de maintenance.
Pour étudier la stabilité des clusters
au cours du temps, nous allons calculer
l'écart entre un predict du modèle initial
et le fit d'un nouveau modèle grâce à
la fonction Ajusted_Rand_Score de la façon suivante:
adjusted_rand_score(labels_true, labels_pred)
La fonction Ajusted_Rand_Score
calcule une mesure de similarité
entre deux clusters en considérant
toutes les paires d'échantillons
et en comptant les paires qui sont
assignées dans le même cluster
ou dans des clusters différents
dans les clusters prédits et réels.
Je nomme :
Nous appellerons les différentes périodes
de temps: t, t+1, t+2,..., t+n
où 'n' représente le nombre de mois
entre la période initiale et la période finale.
Nous commencerons notre clusterisation
avec l'état de la base de données ayant
1an d'existence (t=12 mois)
Nous recommencerons les clusterisations
en ajoutant 1 mois de commande, et ainsi de suite.
A chaque itération, nous comparerons
l'état des clusters.
Lorsque l'Ajusted_Rand_Score obtenu est inférieur à 0.8,
nous considérerons que les clusters ne sont plus stables
et il sera donc nécessaire de réaliser un nouveau
clustering sur les données les plus récentes disponibles.
Nous allons donc calculer la période nécessaire
durant laquelle l'Ajusted_Rand_Score passera
systématiquement sous 0.8 et nous établirons
notre proposition de contrat à partir de cette information.
A la suite aux tests précédents, je décide d'utiliser :
Nous allons recréer le processus nous permettant
de partir des données importées en début de projet
pour arriver au calcul des labels d'un KMeans à
7 Cluster sur un jeu de donnée contenant les
commandes clients d'une période de temps choisie.
Chaque étape du processus est
matérialisée par une fonction à
l'exception de la 1ère étape.
Nous avons besoin des features suivantes :
data = df_customer[['customer_id',
'customer_unique_id']]\
.merge(df_orders[['customer_id',
'order_id',
'order_approved_at',
'order_purchase_timestamp']],
on='customer_id',
how='left')\
.merge(df_reviews[['order_id',
'review_score']],
on='order_id',
how='left')\
.merge(df_payments[['order_id',
'payment_value']],
on='order_id',
how='left')
Je convertis la colonne '**order_purchase_timestamp**'
dans le bon format :
data['order_purchase_timestamp'] = \
pd.to_datetime(data['order_purchase_timestamp'])
def dataByPeriode(myData, nbMonth):
'''
Takes a DataFrame as input
Filters this DataFrame between
the date of the first order
and the last order made 'nbMonth' later.
Returns the filtered DataFrame.
This function is designed to process
a DataFrame that contains the
feature order_purchase_timestamp.
'''
from pandas.tseries.offsets import DateOffset
dateLastOrder = (myData.order_purchase_timestamp.min()
+ DateOffset(months=nbMonth))
return myData[myData.order_purchase_timestamp <= dateLastOrder]
def calculRFAS(myData):
'''
This function is designed to input
a DataFrame from the dataByPeriod function.
This function calculates the 4 main RFAS features
from the features present in the myData DataFrame.
It returns a DataFrame filtered
from the 4 RFAS features.
The 4 features are:
- Recency
- Frequency
- Amount
- Satisfaction
'''
# Recency
lastOrder = myData.order_purchase_timestamp.max()
myData['recency'] = (lastOrder - myData.order_purchase_timestamp)\
.astype('timedelta64[D]').astype(int)
myData = myData.drop('recency',
axis=1)\
.merge(myData[['customer_unique_id',
'recency']]
.groupby('customer_unique_id').min(),
on='customer_unique_id',
how='left')
# Frequency
myData = myData.merge(myData[['customer_unique_id',
'customer_id']]
.groupby('customer_unique_id')
.nunique()
.rename(columns={'customer_id': 'frequency'}),
on='customer_unique_id',
how='left')
# Amout
myData = myData.merge(myData[['customer_unique_id',
'payment_value']]
.groupby('customer_unique_id')
.sum()
.rename(columns={'payment_value': 'amount'}),
on='customer_unique_id',
how='left')
# Satisfaction
myData = myData.merge(myData[['customer_unique_id',
'review_score']]
.groupby('customer_unique_id')
.mean()
.rename(columns={'review_score': 'satisfaction'}),
on='customer_unique_id',
how='left')
return myData[['recency',
'frequency',
'amount',
'satisfaction']]
def stdData(myData, dictDataPrec=''):
'''
This function is designed to accept
a DataFrame from calculRFAS function as input.
It performs a PowerTransformer on the 'recency',
'amount' and 'satisfaction' features
and performs a MinMaxScaler on the 'frequency' feature.
In addition, if a 'dictDataPrec' dictionary
is provided as an argument, the data will be
transformed from the pre-trained models
contained in 'dictDataPrec'.
Otherwise, the models are trained and transformed
from the data contained in 'myData'.
dictDataPrec' must be a dictionary of the
same format as that generated by
this 'stdData' function.
The function returns a dictionary
containing the myData DataFrame and
the two trained models PowerTransformer and MinMaxScaler.
'''
powerT = PowerTransformer().fit(myData[['recency',
'amount',
'satisfaction']])
minmaxS = MinMaxScaler().fit(myData[['frequency']])
if dictDataPrec:
myData[['recency',
'amount',
'satisfaction']] = dictDataPrec['PowerTransform']\
.transform(myData[['recency',
'amount',
'satisfaction']])
myData[['frequency']] = dictDataPrec['MinMaxScaler']\
.transform(myData[['frequency']])
return {'data': myData,
'PowerTransform': powerT,
'MinMaxScaler': minmaxS}
else:
myData[['recency',
'amount',
'satisfaction']] = powerT.transform(myData[['recency',
'amount',
'satisfaction']])
myData[['frequency']] = minmaxS.transform(myData[['frequency']])
return {'data': myData,
'PowerTransform': powerT,
'MinMaxScaler': minmaxS}
def calKMeans(dictAllData, nbCluster):
'''
This function is designed to accept
a Dictionary input from the stdData function.
The function calculates a KMeans with
a number of clusters equal to nbCluster
from the DataFrame contained in
the dictAllData dictionary.
The function adds the trained model
of the KMeans to the dictAllData
dictionary before returning it.
'''
dictAllData['KMeans'] = KMeans(n_clusters=nbCluster)\
.fit(dictAllData['data'])
return dictAllData
def calcARIScore(myData, nbCluster=7, fromNbMonth=12, toNbMonth=15):
'''
This function calculates the Adjusted_Rand_Score
between two different periods of the same myData set.
The periods are calculated in number of months
and are contained in the arguments: 'fromNbMonth' and 'toNbMonth'.
Calculation of two dictionaries, dictDataPeriod0 and dictDataPeriod1,
each containing: myData filtered fromNbMonth for the first
and toNbMonth for the second and their respective
PowerTransformer, MinMaxScaler and KMeans models.
Calculation afterwards:
- labels_true from the dictDataPeriod1 data for the
KMeans training as well as for the data to be processed.
- labels_pred from dictDataPeriod1 data predicted
with a KMeans trained with dictDataPeriod0 data.
The function returns the Ajusted_Rand_Score
calculated between labels_true and labels_pred.
'''
from sklearn.metrics import adjusted_rand_score
dictDataPeriod0 = calKMeans(stdData(
calculRFAS(dataByPeriode(myData,
fromNbMonth))),
nbCluster)
dictDataPeriod1 = calKMeans(stdData(
calculRFAS(dataByPeriode(myData,
toNbMonth)),
dictDataPeriod0),
nbCluster)
labels_true = dictDataPeriod1['KMeans']\
.fit(dictDataPeriod1['data']).labels_
labels_pred = dictDataPeriod0['KMeans']\
.predict(dictDataPeriod1['data'])
return adjusted_rand_score(labels_true, labels_pred)
Nous allons maintenant tracer la stabilité des cluster au cours du temps.
Pour cela, nous calculerons l'Adjusted_Rand_Score sur différentes périodes.
Nous commencerons au plus tôt avec la Base de Données avec 1 année de commandes.
A partir de ce point, nous calculerons l'Adjusted_Rand_Score
successivement en ajoutant 1 mois à chaque itération, sur 5 mois (de t+0 à t+4)
puis nous affichons le score sur un graphique.
Enfin, nous recommencerons le processus en décalant la date de départ
d'un mois (1an + 1mois) et nous répétons la même opération.
Nous réaliserons cette opération 10 fois :
# number of month
fromNbMonthStart = 12
fromNbMonthEnd = 22
plt.figure(figsize=(20, 7))
plt.hlines(0.8, 12, 26, colors='k', linestyles='dashed')
for j in range(fromNbMonthStart, fromNbMonthEnd):
listARIScore = []
for i in range(j, j+5):
listARIScore.append(calcARIScore(data, 7, j, i))
plt.plot(range(j, j+5), listARIScore)
plt.xticks(range(fromNbMonthStart, fromNbMonthEnd+5))
plt.show()
Nous observons que l'Adjusted_Rand_Score n'est pas très stable
au cours du temps et passe la barre de 0.8 après une période
de 1 mois et demi en moyenne.
Dans une simulation de contrat de maintenance, je proposerais
un contrat incluant un recalcul des clusters tous les mois.